@perspective-dev/viewer 4.5.2 → 5.0.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 +20 -1
- 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/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/extensions.d.ts +22 -3
- package/dist/esm/perspective-viewer.d.ts +9 -0
- package/dist/esm/perspective-viewer.inline.js +20 -1
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +20 -1
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +71 -5
- package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
- package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
- package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
- package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
- package/dist/esm/ts-rs/Layout.d.ts +11 -0
- package/dist/esm/ts-rs/Orientation.d.ts +4 -0
- package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
- package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
- package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
- package/dist/wasm/perspective-viewer.d.ts +211 -77
- package/dist/wasm/perspective-viewer.js +528 -180
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-settings-panel.css +1 -1
- package/src/css/containers/context-menu.css +101 -0
- package/src/css/panel-tab.css +193 -0
- package/src/css/plugin-selector.css +7 -10
- package/src/css/status-bar.css +75 -81
- package/src/css/viewer.css +180 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +7 -18
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/config_selector.rs +33 -81
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +3 -8
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +5 -11
- package/src/rust/components/containers/split_panel.rs +175 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/form/debug.rs +3 -0
- package/src/rust/components/global_filter_bar.rs +76 -0
- package/src/rust/components/main_panel/frame_theme.rs +186 -0
- package/src/rust/components/main_panel/msg.rs +62 -0
- package/src/rust/components/main_panel/presize.rs +220 -0
- package/src/rust/components/main_panel/reconcile.rs +224 -0
- package/src/rust/components/main_panel/render.rs +385 -0
- package/src/rust/components/main_panel/update.rs +184 -0
- package/src/rust/components/main_panel.rs +311 -71
- package/src/rust/components/mod.rs +4 -0
- package/src/rust/components/panel_menu.rs +486 -0
- package/src/rust/components/panel_tab.rs +505 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +110 -178
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/style/mod.rs +5 -0
- package/src/rust/components/style/style_provider.rs +2 -0
- package/src/rust/components/viewer/filters.rs +133 -0
- package/src/rust/components/viewer/msg.rs +180 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +243 -0
- package/src/rust/components/viewer/settings.rs +402 -0
- package/src/rust/components/viewer/snapshots.rs +134 -0
- package/src/rust/components/viewer/wiring.rs +505 -0
- package/src/rust/components/viewer.rs +243 -756
- package/src/rust/config/export_method.rs +1 -1
- package/src/rust/config/mod.rs +4 -0
- package/src/rust/config/options.rs +71 -0
- package/src/rust/config/viewer_config.rs +24 -2
- package/src/rust/config/workspace_config.rs +92 -0
- package/src/rust/custom_elements/copy_dropdown.rs +3 -1
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +3 -1
- package/src/rust/custom_elements/viewer.rs +1015 -286
- package/src/rust/custom_events.rs +212 -61
- package/src/rust/js/mod.rs +2 -0
- package/src/rust/js/plugin.rs +297 -10
- package/src/rust/js/regular_layout.rs +258 -0
- package/src/rust/lib.rs +20 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/get_viewer_config.rs +29 -11
- package/src/rust/queries/plugin_column_styles.rs +2 -2
- package/src/rust/renderer/activate.rs +41 -14
- package/src/rust/renderer/dispatch.rs +436 -0
- package/src/rust/renderer/plugin_config.rs +495 -0
- package/src/rust/renderer/registry.rs +18 -6
- package/src/rust/renderer/render_timer.rs +29 -10
- package/src/rust/renderer.rs +350 -837
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +753 -264
- package/src/rust/tasks/apply_global_filters.rs +89 -0
- package/src/rust/tasks/auto_pause.rs +171 -0
- package/src/rust/tasks/copy_export.rs +29 -19
- package/src/rust/tasks/create_panel.rs +243 -0
- package/src/rust/tasks/dismiss_render_warning.rs +3 -1
- package/src/rust/tasks/edit_expression.rs +4 -4
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +28 -4
- package/src/rust/tasks/pipeline.rs +411 -0
- package/src/rust/tasks/presize_panels.rs +324 -0
- package/src/rust/tasks/reset_all.rs +22 -12
- package/src/rust/tasks/resize_observer.rs +124 -87
- package/src/rust/tasks/restore_and_render.rs +80 -86
- package/src/rust/tasks/restore_panel.rs +145 -0
- package/src/rust/tasks/send_column_config.rs +5 -2
- package/src/rust/tasks/send_plugin_config.rs +5 -2
- package/src/rust/tasks/set_edit_mode.rs +59 -0
- package/src/rust/tasks/sync_update_panels.rs +152 -0
- package/src/rust/tasks/table_lifecycle.rs +158 -0
- package/src/rust/tasks/update_theme.rs +56 -12
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +130 -7
- package/src/rust/utils/mod.rs +6 -0
- package/src/rust/utils/pending_effects.rs +62 -0
- package/src/rust/utils/spawn.rs +33 -0
- package/src/rust/utils/weak_scope.rs +0 -10
- package/src/rust/workspace.rs +846 -0
- package/src/svg/active-indicator.svg +3 -0
- package/src/themes/botanical.css +5 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +6 -3
- package/src/themes/gruvbox-dark.css +5 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +4 -2
- package/src/themes/monokai.css +5 -2
- package/src/themes/phosphor.css +5 -2
- package/src/themes/pro-dark.css +5 -2
- package/src/themes/pro.css +2 -1
- package/src/themes/solarized-dark.css +5 -2
- package/src/themes/solarized.css +5 -2
- package/src/themes/vaporwave.css +5 -2
- package/src/ts/bootstrap.ts +8 -7
- package/src/ts/extensions.ts +30 -2
- package/src/ts/perspective-viewer.ts +10 -0
- package/src/ts/plugin.ts +72 -5
- package/src/ts/ts-rs/ClientOptions.ts +7 -0
- package/src/ts/ts-rs/ExportMethod.ts +3 -0
- package/src/ts/ts-rs/ExportOptions.ts +8 -0
- package/src/ts/ts-rs/GetClientOptions.ts +7 -0
- package/src/ts/ts-rs/GetTableOptions.ts +7 -0
- package/src/ts/ts-rs/Layout.ts +9 -0
- package/src/ts/ts-rs/Orientation.ts +6 -0
- package/src/ts/ts-rs/PanelOptions.ts +7 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
- package/src/ts/ts-rs/SplitLayout.ts +5 -0
- package/src/ts/ts-rs/TabLayout.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +6 -2
- package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
- package/src/rust/tasks/intersection_observer.rs +0 -98
- package/src/rust/tasks/update_and_render.rs +0 -110
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
package/src/rust/renderer.rs
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
//! references throughout the application.
|
|
19
19
|
|
|
20
20
|
mod activate;
|
|
21
|
+
mod dispatch;
|
|
21
22
|
pub mod limits;
|
|
23
|
+
mod plugin_config;
|
|
22
24
|
mod plugin_store;
|
|
23
25
|
mod props;
|
|
24
26
|
mod registry;
|
|
@@ -26,17 +28,11 @@ mod render_timer;
|
|
|
26
28
|
|
|
27
29
|
use std::cell::{Cell, RefCell};
|
|
28
30
|
use std::collections::HashMap;
|
|
29
|
-
use std::future::Future;
|
|
30
31
|
use std::ops::Deref;
|
|
31
|
-
use std::pin::Pin;
|
|
32
32
|
use std::rc::Rc;
|
|
33
33
|
|
|
34
|
-
use
|
|
35
|
-
use
|
|
36
|
-
use perspective_client::utils::*;
|
|
37
|
-
use perspective_client::{View, ViewWindow};
|
|
38
|
-
use perspective_js::utils::{ApiResult, JsValueSerdeExt, ResultTApiErrorExt};
|
|
39
|
-
use serde_json::Value;
|
|
34
|
+
use perspective_client::ViewWindow;
|
|
35
|
+
use perspective_js::utils::{ApiFuture, ApiResult, JsValueSerdeExt};
|
|
40
36
|
use wasm_bindgen::prelude::*;
|
|
41
37
|
use web_sys::*;
|
|
42
38
|
use yew::html::ImplicitClone;
|
|
@@ -44,31 +40,43 @@ use yew::prelude::*;
|
|
|
44
40
|
|
|
45
41
|
use self::activate::*;
|
|
46
42
|
pub use self::limits::RenderLimits;
|
|
47
|
-
use self::
|
|
43
|
+
pub use self::plugin_config::{ColumnConfigMap, PluginScopedConfig};
|
|
48
44
|
use self::plugin_store::*;
|
|
49
45
|
pub use self::props::RendererProps;
|
|
50
46
|
pub use self::registry::*;
|
|
51
47
|
use self::render_timer::*;
|
|
52
48
|
use crate::config::*;
|
|
53
49
|
use crate::js::plugin::*;
|
|
54
|
-
use crate::queries::resolve_abs_max;
|
|
55
|
-
use crate::session::Session;
|
|
56
50
|
use crate::utils::*;
|
|
57
51
|
|
|
58
|
-
///
|
|
59
|
-
///
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
///
|
|
64
|
-
///
|
|
65
|
-
///
|
|
66
|
-
///
|
|
67
|
-
///
|
|
68
|
-
|
|
69
|
-
pub struct
|
|
70
|
-
pub
|
|
71
|
-
pub
|
|
52
|
+
/// Minimum geometry delta (px) considered a real size/position change by the
|
|
53
|
+
/// presize paths.
|
|
54
|
+
pub(crate) const SUBPIXEL_EPSILON: f64 = 0.5;
|
|
55
|
+
|
|
56
|
+
/// Everything a plugin may read back during its render, all belonging to
|
|
57
|
+
/// one snapshot (invariant I5). Built once per bound `View` by the render
|
|
58
|
+
/// pipeline and CACHED on the `Renderer`; pinning it for a run is an `Rc`
|
|
59
|
+
/// clone (cheap enough for resize-tick-rate runs). While pinned, the
|
|
60
|
+
/// per-panel element getters (`getViewConfigPanel`, `getTablePanel`,
|
|
61
|
+
/// `getClientPanel`, `getEditPortPanel`) answer from it — the plugin ABI is
|
|
62
|
+
/// inside the snapshot boundary.
|
|
63
|
+
pub struct RenderContext {
|
|
64
|
+
pub view_config: Rc<perspective_client::config::ViewConfig>,
|
|
65
|
+
pub view: perspective_client::View,
|
|
66
|
+
pub table: perspective_client::Table,
|
|
67
|
+
pub client: perspective_client::Client,
|
|
68
|
+
pub edit_port: Option<f64>,
|
|
69
|
+
pub theme: Option<String>,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// RAII pin for a [`RenderContext`] — cleared on drop, INCLUDING error
|
|
73
|
+
/// exits, so a failed run can't leak a stale context.
|
|
74
|
+
pub struct ContextPin(Renderer);
|
|
75
|
+
|
|
76
|
+
impl Drop for ContextPin {
|
|
77
|
+
fn drop(&mut self) {
|
|
78
|
+
self.0.0.active_context.borrow_mut().take();
|
|
79
|
+
}
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/// Immutable state
|
|
@@ -92,8 +100,84 @@ pub struct RendererData {
|
|
|
92
100
|
/// dismissable.
|
|
93
101
|
pub render_warning: Cell<bool>,
|
|
94
102
|
|
|
103
|
+
/// Count of in-flight [`Renderer::resize_with_dimensions`] /
|
|
104
|
+
/// [`Renderer::presize_with_box`] presize calls.
|
|
105
|
+
presize_pending: Cell<u32>,
|
|
106
|
+
|
|
95
107
|
/// Fires after every draw/update with the computed render limits.
|
|
96
108
|
pub on_render_limits_changed: RefCell<Option<Callback<RenderLimits>>>,
|
|
109
|
+
|
|
110
|
+
/// The layout slot name (panel id) under which this renderer mounts its
|
|
111
|
+
/// plugin in the viewer's light DOM, so multiple panels' plugins can
|
|
112
|
+
/// coexist there.
|
|
113
|
+
slot_name: RefCell<Option<String>>,
|
|
114
|
+
|
|
115
|
+
/// This panel's selected theme name (per-panel theming), or `None` to
|
|
116
|
+
/// inherit the element-level (active) theme.
|
|
117
|
+
theme: RefCell<Option<String>>,
|
|
118
|
+
|
|
119
|
+
/// The registry default theme name (first registered), cached here so
|
|
120
|
+
/// LOCKED draw paths can resolve this panel's EFFECTIVE theme
|
|
121
|
+
/// synchronously (`Presentation::get_default_theme_name` is async — it
|
|
122
|
+
/// awaits theme discovery). Seeded by every content-load path before its
|
|
123
|
+
/// first locked draw (`restore_and_render`, `load()`, the
|
|
124
|
+
/// resize-observer's deferred first render) and kept fresh by the root's
|
|
125
|
+
/// `UpdatePresentation` default-theme fan-out and `resetThemes`.
|
|
126
|
+
default_theme: RefCell<Option<String>>,
|
|
127
|
+
|
|
128
|
+
/// Whether the active plugin has completed a draw. An EXPLICIT flag —
|
|
129
|
+
/// not inferred from DOM connectedness — because plugin elements may be
|
|
130
|
+
/// mounted eagerly (at panel creation / draw start, before the view
|
|
131
|
+
/// query resolves), so "in the DOM" no longer implies "has rendered".
|
|
132
|
+
/// Set by a successful `draw_view`; cleared on plugin swap
|
|
133
|
+
/// (`commit_plugin_idx`), `dispose` and `delete`.
|
|
134
|
+
has_drawn: Cell<bool>,
|
|
135
|
+
|
|
136
|
+
/// The effective theme stamped at the active plugin's last `--psp-*`
|
|
137
|
+
/// CSS CAPTURE — its first paint ([`Renderer::draw_view`]) or last
|
|
138
|
+
/// [`Renderer::restyle_all`]. `None` = no capture has happened. The
|
|
139
|
+
/// state behind [`Renderer::needs_restyle`]: restyle necessity is a
|
|
140
|
+
/// comparison against what the plugin actually captured, never
|
|
141
|
+
/// inferred from which call site mutated a theme record (the
|
|
142
|
+
/// raycasting boot double render — see
|
|
143
|
+
/// `PLUGIN_DRAW_INVARIANT_PLAN.md`, captured-theme revision). Same
|
|
144
|
+
/// lifecycle as [`Self::has_drawn`]: cleared on plugin swap,
|
|
145
|
+
/// `dispose` and `delete`; written only inside locked dispatches.
|
|
146
|
+
captured_theme: RefCell<Option<Option<String>>>,
|
|
147
|
+
|
|
148
|
+
/// The [`RenderContext`] of the currently-bound `View` (built at bind
|
|
149
|
+
/// time by the pipeline). Cleared on `dispose`/`delete`.
|
|
150
|
+
cached_context: RefCell<Option<Rc<RenderContext>>>,
|
|
151
|
+
|
|
152
|
+
/// The [`RenderContext`] pinned by the run currently holding the draw
|
|
153
|
+
/// lock, if any (invariant I5). Managed by [`ContextPin`].
|
|
154
|
+
active_context: RefCell<Option<Rc<RenderContext>>>,
|
|
155
|
+
|
|
156
|
+
/// Whether this renderer's panel is the workspace's ACTIVE panel. Pure
|
|
157
|
+
/// DATA, written synchronously by `Workspace::set_active`/`insert_panel`;
|
|
158
|
+
/// the `active` CSS class it drives is applied ONLY inside locked plugin
|
|
159
|
+
/// dispatches ([`Renderer::stamp_active`], "stamp before draw" — like the
|
|
160
|
+
/// theme), so activation-dependent chrome and the DOM it styles always
|
|
161
|
+
/// land in one paint commit.
|
|
162
|
+
is_active_panel: Cell<bool>,
|
|
163
|
+
|
|
164
|
+
/// Whether this renderer's panel is the workspace's ONLY panel. Pure DATA
|
|
165
|
+
/// like [`Self::is_active_panel`], written synchronously by the
|
|
166
|
+
/// `Workspace` panel-count mutation sites (`new`/`insert_panel`/
|
|
167
|
+
/// `remove_panel`); the `single`/`multi` CSS classes it drives are
|
|
168
|
+
/// applied ONLY inside locked plugin dispatches
|
|
169
|
+
/// ([`Renderer::stamp_active`]), for the same one-paint-commit reason.
|
|
170
|
+
is_solo_panel: Cell<bool>,
|
|
171
|
+
|
|
172
|
+
/// A `table_updated` redraw was DROPPED because this panel's plugin was
|
|
173
|
+
/// hidden (an unslotted tab-stack panel — see
|
|
174
|
+
/// `tasks::create_panel::wire_panel_render_sub`), so the plugin's
|
|
175
|
+
/// retained frame no longer reflects the bound `View`'s data. The
|
|
176
|
+
/// activation nudge consumes it to dispatch a full `plugin.update`
|
|
177
|
+
/// instead of the usual chrome-only `resize` repaint. Cleared at every
|
|
178
|
+
/// `draw_view` dispatch — set BEFORE the plugin's data fetch, so an
|
|
179
|
+
/// update landing mid-draw re-marks it and can never be lost.
|
|
180
|
+
data_stale: Cell<bool>,
|
|
97
181
|
}
|
|
98
182
|
|
|
99
183
|
/// Mutable state
|
|
@@ -104,9 +188,6 @@ pub struct RendererMutData {
|
|
|
104
188
|
plugins_idx: Option<usize>,
|
|
105
189
|
timer: MovingWindowRenderTimer,
|
|
106
190
|
selection: Option<ViewWindow>,
|
|
107
|
-
pending_plugin: Option<usize>,
|
|
108
|
-
|
|
109
|
-
/// Per-plugin config buckets, keyed by plugin name.
|
|
110
191
|
plugin_states: HashMap<String, PluginScopedConfig>,
|
|
111
192
|
}
|
|
112
193
|
|
|
@@ -138,12 +219,6 @@ impl Deref for RendererData {
|
|
|
138
219
|
}
|
|
139
220
|
}
|
|
140
221
|
|
|
141
|
-
type TaskResult = ApiResult<JsValue>;
|
|
142
|
-
type TimeoutTask<'a> = Pin<Box<dyn Future<Output = Option<TaskResult>> + 'a>>;
|
|
143
|
-
|
|
144
|
-
/// How long to await a call to the plugin's `draw()` before resizing.
|
|
145
|
-
static PRESIZE_TIMEOUT: i32 = 500;
|
|
146
|
-
|
|
147
222
|
impl Renderer {
|
|
148
223
|
pub fn new(viewer_elem: &HtmlElement) -> Self {
|
|
149
224
|
Self(Rc::new(RendererData {
|
|
@@ -154,7 +229,6 @@ impl Renderer {
|
|
|
154
229
|
plugins_idx: None,
|
|
155
230
|
selection: None,
|
|
156
231
|
timer: MovingWindowRenderTimer::default(),
|
|
157
|
-
pending_plugin: None,
|
|
158
232
|
plugin_states: HashMap::default(),
|
|
159
233
|
}),
|
|
160
234
|
draw_lock: Default::default(),
|
|
@@ -165,524 +239,176 @@ impl Renderer {
|
|
|
165
239
|
column_style_changed: Default::default(),
|
|
166
240
|
plugin_config_changed: Default::default(),
|
|
167
241
|
render_warning: Cell::new(true),
|
|
242
|
+
presize_pending: Cell::new(0),
|
|
168
243
|
on_render_limits_changed: Default::default(),
|
|
244
|
+
slot_name: Default::default(),
|
|
245
|
+
theme: Default::default(),
|
|
246
|
+
default_theme: Default::default(),
|
|
247
|
+
has_drawn: Cell::new(false),
|
|
248
|
+
captured_theme: Default::default(),
|
|
249
|
+
cached_context: Default::default(),
|
|
250
|
+
active_context: Default::default(),
|
|
251
|
+
is_active_panel: Cell::new(false),
|
|
252
|
+
is_solo_panel: Cell::new(true),
|
|
253
|
+
data_stale: Cell::new(false),
|
|
169
254
|
}))
|
|
170
255
|
}
|
|
171
256
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
&mut *self.plugin_data.borrow_mut(),
|
|
178
|
-
&mut *new_state.plugin_data.borrow_mut(),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
Ok(())
|
|
257
|
+
/// Set the layout slot name (panel id) under which this renderer mounts its
|
|
258
|
+
/// plugin in the viewer's light DOM, routed through the matching
|
|
259
|
+
/// `<regular-layout>` forwarding slot.
|
|
260
|
+
pub fn set_slot_name(&self, name: &str) {
|
|
261
|
+
*self.0.slot_name.borrow_mut() = Some(name.to_owned());
|
|
182
262
|
}
|
|
183
263
|
|
|
184
|
-
|
|
185
|
-
|
|
264
|
+
/// The layout slot name (panel id) for this renderer's plugin, if assigned.
|
|
265
|
+
pub fn slot_name(&self) -> Option<String> {
|
|
266
|
+
self.0.slot_name.borrow().clone()
|
|
186
267
|
}
|
|
187
268
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
/// Whether the active plugin opts into per-column style controls.
|
|
193
|
-
pub fn can_render_column_styles(&self) -> bool {
|
|
194
|
-
self.metadata().can_render_column_styles
|
|
269
|
+
/// Set this panel's theme name (`None` = inherit the element-level theme).
|
|
270
|
+
pub fn set_theme(&self, name: Option<String>) {
|
|
271
|
+
*self.0.theme.borrow_mut() = name;
|
|
195
272
|
}
|
|
196
273
|
|
|
197
|
-
///
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
fn active_plugin_name(&self) -> Option<String> {
|
|
201
|
-
Some(self.borrow().metadata.name.clone()).filter(|n| !n.is_empty())
|
|
274
|
+
/// This panel's selected theme name, if any (per-panel theming).
|
|
275
|
+
pub fn theme(&self) -> Option<String> {
|
|
276
|
+
self.0.theme.borrow().clone()
|
|
202
277
|
}
|
|
203
278
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
///
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
})
|
|
215
|
-
.unwrap_or_default()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/// Restore-prep snapshot: like [`Self::all_columns_configs`], but
|
|
219
|
-
/// for each column also materializes any `ControlSpec::Number`
|
|
220
|
-
/// fields the schema declares with `include: true` that aren't
|
|
221
|
-
/// already in the bucket entry. The materialized value is the
|
|
222
|
-
/// schema's `default`, which the schema computes from cached
|
|
223
|
-
/// column stats (via [`Self::query_column_config_schema`]).
|
|
224
|
-
///
|
|
225
|
-
/// The bucket itself stays minimal (user edits + `include: true`
|
|
226
|
-
/// values the user *explicitly set*); this helper produces the
|
|
227
|
-
/// fully-realized payload the plugin's `restore` is expected to
|
|
228
|
-
/// receive. Every restore-prep site should call this rather than
|
|
229
|
-
/// `all_columns_configs` directly — otherwise widgets that gate
|
|
230
|
-
/// `include` fields off other fields (e.g. Datagrid's
|
|
231
|
-
/// `fg_gradient` revealed when `number_fg_mode = "bar"`) will
|
|
232
|
-
/// reach the plugin without their default value populated.
|
|
233
|
-
///
|
|
234
|
-
/// `async` because per-column stats (e.g. `abs_max` for Datagrid's
|
|
235
|
-
/// `fg_gradient`) may need to be fetched before the schema's
|
|
236
|
-
/// `default` is meaningful. Pass 1 sync-scans the schema for any
|
|
237
|
-
/// column whose `include: true` Number key is missing from its
|
|
238
|
-
/// entry AND has no cached stats — `view_config_changed` clears the
|
|
239
|
-
/// stats cache, so "missing in cache" subsumes "stale". Pass 2
|
|
240
|
-
/// blocks on a parallel `resolve_abs_max` for that set, then runs
|
|
241
|
-
/// the materialize loop with the now-warm cache. Columns never
|
|
242
|
-
/// touched in a stats-dependent mode never trigger a fetch.
|
|
243
|
-
pub async fn all_columns_configs_materialized(
|
|
244
|
-
&self,
|
|
245
|
-
view_config: &ViewConfig,
|
|
246
|
-
session: &Session,
|
|
247
|
-
) -> ColumnConfigMap {
|
|
248
|
-
let mut configs = self.all_columns_configs();
|
|
249
|
-
|
|
250
|
-
// Pass 1: identify columns whose schema demands an `include:
|
|
251
|
-
// true` Number default we don't have stats for.
|
|
252
|
-
let mut to_warm: Vec<String> = vec![];
|
|
253
|
-
for (col, entry) in &configs {
|
|
254
|
-
if session
|
|
255
|
-
.get_column_stats(col)
|
|
256
|
-
.and_then(|s| s.abs_max)
|
|
257
|
-
.is_some()
|
|
258
|
-
{
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
let Ok(schema) =
|
|
262
|
-
self.query_column_config_schema(view_config, session, col, Some(entry))
|
|
263
|
-
else {
|
|
264
|
-
continue;
|
|
265
|
-
};
|
|
266
|
-
let needs_warm = schema.fields.iter().any(|f| {
|
|
267
|
-
matches!(
|
|
268
|
-
f,
|
|
269
|
-
ControlSpec::Number {
|
|
270
|
-
key,
|
|
271
|
-
include: Some(true),
|
|
272
|
-
..
|
|
273
|
-
} if !entry.contains_key(key)
|
|
274
|
-
)
|
|
275
|
-
});
|
|
276
|
-
if needs_warm {
|
|
277
|
-
to_warm.push(col.clone());
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Block on the (typically tiny) warm set. Clone the metadata
|
|
282
|
-
// and resolve the view ref *before* the .await — `metadata()`
|
|
283
|
-
// returns a live `Ref<>` guard that must not cross an await
|
|
284
|
-
// boundary.
|
|
285
|
-
if !to_warm.is_empty() {
|
|
286
|
-
let metadata = session.metadata().clone();
|
|
287
|
-
let view = session.get_view();
|
|
288
|
-
let futs = to_warm
|
|
289
|
-
.iter()
|
|
290
|
-
.map(|c| resolve_abs_max(session, &metadata, view.as_ref(), c.as_str()));
|
|
291
|
-
join_all(futs).await;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Pass 2: materialize. With stats now in cache, the schema
|
|
295
|
-
// returns a real `default` instead of the placeholder `0`.
|
|
296
|
-
for (col, entry) in &mut configs {
|
|
297
|
-
let Ok(schema) =
|
|
298
|
-
self.query_column_config_schema(view_config, session, col, Some(entry))
|
|
299
|
-
else {
|
|
300
|
-
continue;
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
for field in &schema.fields {
|
|
304
|
-
let ControlSpec::Number {
|
|
305
|
-
key,
|
|
306
|
-
default,
|
|
307
|
-
include: Some(true),
|
|
308
|
-
..
|
|
309
|
-
} = field
|
|
310
|
-
else {
|
|
311
|
-
continue;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
if entry.contains_key(key) {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let Some(num) = serde_json::Number::from_f64(*default) else {
|
|
319
|
-
continue;
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
entry.insert(key.clone(), serde_json::Value::Number(num));
|
|
323
|
-
}
|
|
279
|
+
/// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] when the
|
|
280
|
+
/// effective theme is resolvable now (a named theme needs no registry; a
|
|
281
|
+
/// reset stamps only from a warm default cache — a cold one would stamp
|
|
282
|
+
/// attribute-removal). The shared "stamp-with-commit" head of every
|
|
283
|
+
/// per-panel theme mutation site.
|
|
284
|
+
pub fn set_theme_stamped(&self, theme: Option<String>) {
|
|
285
|
+
let stamp = theme.is_some() || self.default_theme().is_some();
|
|
286
|
+
self.set_theme(theme);
|
|
287
|
+
if stamp {
|
|
288
|
+
self.stamp_theme(None);
|
|
324
289
|
}
|
|
325
|
-
|
|
326
|
-
configs
|
|
327
290
|
}
|
|
328
291
|
|
|
329
|
-
///
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
.plugin_states
|
|
334
|
-
.entry(n)
|
|
335
|
-
.or_default()
|
|
336
|
-
.columns
|
|
337
|
-
.clear();
|
|
338
|
-
}
|
|
292
|
+
/// Set the cached registry default theme name (see the field docs on
|
|
293
|
+
/// [`RendererData`]).
|
|
294
|
+
pub fn set_default_theme(&self, name: Option<String>) {
|
|
295
|
+
*self.0.default_theme.borrow_mut() = name;
|
|
339
296
|
}
|
|
340
297
|
|
|
341
|
-
///
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
&self,
|
|
345
|
-
column_name: &str,
|
|
346
|
-
) -> Option<serde_json::Map<String, serde_json::Value>> {
|
|
347
|
-
let n = self.active_plugin_name()?;
|
|
348
|
-
self.borrow()
|
|
349
|
-
.plugin_states
|
|
350
|
-
.get(&n)?
|
|
351
|
-
.columns
|
|
352
|
-
.get(column_name)
|
|
353
|
-
.cloned()
|
|
298
|
+
/// The cached registry default theme name.
|
|
299
|
+
pub fn default_theme(&self) -> Option<String> {
|
|
300
|
+
self.0.default_theme.borrow().clone()
|
|
354
301
|
}
|
|
355
302
|
|
|
356
|
-
///
|
|
357
|
-
///
|
|
358
|
-
///
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
/// declared with `include: true` survive the strip (their default
|
|
363
|
-
/// is data-dependent, so a literal default value is preserved as
|
|
364
|
-
/// the user's explicit choice). Column entries that become empty
|
|
365
|
-
/// after stripping are removed from the bucket entirely.
|
|
366
|
-
pub fn update_columns_configs(
|
|
367
|
-
&self,
|
|
368
|
-
view_config: &ViewConfig,
|
|
369
|
-
session: &Session,
|
|
370
|
-
update: ColumnConfigUpdate,
|
|
371
|
-
) -> bool {
|
|
372
|
-
let Some(n) = self.active_plugin_name() else {
|
|
373
|
-
return false;
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
match update {
|
|
377
|
-
OptionalUpdate::SetDefault => {
|
|
378
|
-
let mut st = self.borrow_mut();
|
|
379
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
380
|
-
let was_nonempty = !bucket.columns.is_empty();
|
|
381
|
-
bucket.columns.clear();
|
|
382
|
-
was_nonempty
|
|
383
|
-
},
|
|
384
|
-
OptionalUpdate::Missing => false,
|
|
385
|
-
OptionalUpdate::Update(map) => {
|
|
386
|
-
// Strip per-column before borrowing the bucket mutably:
|
|
387
|
-
// the schema query takes an immutable borrow via
|
|
388
|
-
// `self.metadata()`, which would alias-conflict with
|
|
389
|
-
// `borrow_mut` below.
|
|
390
|
-
let stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> = map
|
|
391
|
-
.into_iter()
|
|
392
|
-
.map(|(col, mut cfg)| {
|
|
393
|
-
if let Ok(schema) =
|
|
394
|
-
self.query_column_config_schema(view_config, session, &col, Some(&cfg))
|
|
395
|
-
{
|
|
396
|
-
let active = schema.active_keys();
|
|
397
|
-
cfg.retain(|k, _| active.contains(k));
|
|
398
|
-
strip_default_values(&schema, &mut cfg);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
(col, cfg)
|
|
402
|
-
})
|
|
403
|
-
.collect();
|
|
404
|
-
|
|
405
|
-
let mut st = self.borrow_mut();
|
|
406
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
407
|
-
let mut changed = false;
|
|
408
|
-
for (col, cfg) in stripped {
|
|
409
|
-
if cfg.is_empty() {
|
|
410
|
-
if bucket.columns.remove(&col).is_some() {
|
|
411
|
-
changed = true;
|
|
412
|
-
}
|
|
413
|
-
} else {
|
|
414
|
-
match bucket.columns.insert(col, cfg.clone()) {
|
|
415
|
-
None => changed = true,
|
|
416
|
-
Some(old) if old != cfg => changed = true,
|
|
417
|
-
_ => {},
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
303
|
+
/// This panel's EFFECTIVE theme: its own ([`Self::theme`]), else the
|
|
304
|
+
/// cached registry default ([`Self::default_theme`]) — the value
|
|
305
|
+
/// [`Self::stamp_theme`] stamps.
|
|
306
|
+
pub fn effective_theme(&self) -> Option<String> {
|
|
307
|
+
self.theme().or_else(|| self.default_theme())
|
|
308
|
+
}
|
|
421
309
|
|
|
422
|
-
|
|
423
|
-
|
|
310
|
+
/// Whether the active plugin's captured `--psp-*` CSS is STALE — the
|
|
311
|
+
/// effective theme differs from the one stamped at the plugin's last
|
|
312
|
+
/// CSS capture (first paint / last restyle). The state-keyed gate for
|
|
313
|
+
/// every theme-driven `restyle_all` (the `has_drawn` analog for
|
|
314
|
+
/// `restyle` — `PLUGIN_DRAW_INVARIANT_PLAN.md`, captured-theme
|
|
315
|
+
/// revision): a caller that just mutated a theme RECORD may not need a
|
|
316
|
+
/// restyle at all if the plugin's capture already reflects the new
|
|
317
|
+
/// value (e.g. the same restore also performed the first paint,
|
|
318
|
+
/// post-stamp). `false` when no capture exists yet — the owed first
|
|
319
|
+
/// paint captures fresh by construction ("stamp before draw").
|
|
320
|
+
pub fn needs_restyle(&self) -> bool {
|
|
321
|
+
match &*self.0.captured_theme.borrow() {
|
|
322
|
+
Some(captured) => *captured != self.effective_theme(),
|
|
323
|
+
None => false,
|
|
424
324
|
}
|
|
425
325
|
}
|
|
426
326
|
|
|
427
|
-
///
|
|
428
|
-
///
|
|
429
|
-
///
|
|
430
|
-
///
|
|
327
|
+
/// Dispose a single panel's renderer: delete its active plugin and remove
|
|
328
|
+
/// this panel's element(s) from the viewer's light DOM, scoped by
|
|
329
|
+
/// `slot_name` so sibling panels are untouched (unlike
|
|
330
|
+
/// [`Self::delete`], which clears the entire light DOM). Removes the
|
|
331
|
+
/// plugin (`slot=<id>`) and any panel-scoped aux element it mounted
|
|
332
|
+
/// (e.g. the datagrid toolbar, `slot=statusbar-extra-<id>`).
|
|
431
333
|
///
|
|
432
|
-
/// The
|
|
433
|
-
///
|
|
434
|
-
///
|
|
435
|
-
///
|
|
436
|
-
/// `
|
|
437
|
-
///
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
view_config,
|
|
454
|
-
session,
|
|
455
|
-
&column_name,
|
|
456
|
-
current_value.as_ref(),
|
|
457
|
-
) {
|
|
458
|
-
strip_default_values(&schema, &mut update.value);
|
|
459
|
-
}
|
|
334
|
+
/// The teardown is DEFERRED through this renderer's draw lock: spawned
|
|
335
|
+
/// draws are uncancellable, so a synchronous `plugin.delete()` here could
|
|
336
|
+
/// land mid-`plugin.draw()`, which the serialized-call contract forbids
|
|
337
|
+
/// (see [`crate::js::plugin`]). Deferral is invisible — the caller
|
|
338
|
+
/// (`eject_panel`) has already removed the panel from the `Workspace`, so
|
|
339
|
+
/// the same commit unmounts its frame and the plugin element sits
|
|
340
|
+
/// unslotted (unrendered) until teardown lands; panel ids are never
|
|
341
|
+
/// reused, so it can't collide with a replacement panel's slot. Draws
|
|
342
|
+
/// still queued behind the lock no-op via the synchronous session reset
|
|
343
|
+
/// (`get_view()` → `None`).
|
|
344
|
+
pub fn dispose(&self) -> ApiFuture<()> {
|
|
345
|
+
self.0.has_drawn.set(false);
|
|
346
|
+
self.0.captured_theme.borrow_mut().take();
|
|
347
|
+
self.0.cached_context.borrow_mut().take();
|
|
348
|
+
let this = self.clone();
|
|
349
|
+
ApiFuture::new(async move {
|
|
350
|
+
let renderer = this.clone();
|
|
351
|
+
this.with_lock(async move {
|
|
352
|
+
if let Some(plugin) = renderer.active_plugin() {
|
|
353
|
+
plugin.delete();
|
|
354
|
+
}
|
|
460
355
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
for k in &update.keys {
|
|
465
|
-
entry.remove(k);
|
|
466
|
-
}
|
|
467
|
-
for (k, v) in update.value {
|
|
468
|
-
if update.keys.contains(&k) {
|
|
469
|
-
entry.insert(k, v);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if entry.is_empty() {
|
|
473
|
-
bucket.columns.remove(&column_name);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
356
|
+
let Some(slot) = renderer.slot_name() else {
|
|
357
|
+
return Ok(());
|
|
358
|
+
};
|
|
476
359
|
|
|
477
|
-
|
|
360
|
+
let viewer = renderer.plugin_data.borrow().viewer_elem.clone();
|
|
361
|
+
let slots = [slot.clone(), format!("statusbar-extra-{slot}")];
|
|
362
|
+
let children = viewer.children();
|
|
363
|
+
let mut idx = children.length();
|
|
364
|
+
while idx > 0 {
|
|
365
|
+
idx -= 1;
|
|
366
|
+
if let Some(el) = children.item(idx)
|
|
367
|
+
&& el.get_attribute("slot").is_some_and(|s| slots.contains(&s))
|
|
368
|
+
{
|
|
369
|
+
let _ = viewer.remove_child(&el);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
478
372
|
|
|
479
|
-
|
|
480
|
-
pub fn get_plugin_config(&self) -> serde_json::Map<String, serde_json::Value> {
|
|
481
|
-
self.active_plugin_name()
|
|
482
|
-
.and_then(|n| {
|
|
483
|
-
self.borrow()
|
|
484
|
-
.plugin_states
|
|
485
|
-
.get(&n)
|
|
486
|
-
.map(|b| b.plugin.clone())
|
|
373
|
+
Ok(())
|
|
487
374
|
})
|
|
488
|
-
.
|
|
375
|
+
.await
|
|
376
|
+
})
|
|
489
377
|
}
|
|
490
378
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
.or_default()
|
|
498
|
-
.plugin
|
|
499
|
-
.clear();
|
|
379
|
+
pub fn delete(&self) -> ApiResult<()> {
|
|
380
|
+
self.0.has_drawn.set(false);
|
|
381
|
+
self.0.captured_theme.borrow_mut().take();
|
|
382
|
+
self.0.cached_context.borrow_mut().take();
|
|
383
|
+
if let Some(plugin) = self.active_plugin() {
|
|
384
|
+
plugin.delete();
|
|
500
385
|
}
|
|
501
|
-
|
|
386
|
+
self.plugin_data.borrow().viewer_elem.set_inner_text("");
|
|
387
|
+
let new_state = Self::new(&self.plugin_data.borrow().viewer_elem);
|
|
388
|
+
std::mem::swap(
|
|
389
|
+
&mut *self.plugin_data.borrow_mut(),
|
|
390
|
+
&mut *new_state.plugin_data.borrow_mut(),
|
|
391
|
+
);
|
|
502
392
|
|
|
503
|
-
|
|
504
|
-
/// [`ColumnConfigSchema`] used to gate plugin-config strip logic.
|
|
505
|
-
/// Inlined here (rather than calling `queries::get_plugin_config_schema`)
|
|
506
|
-
/// to keep `renderer` from back-importing the `queries` module.
|
|
507
|
-
fn query_plugin_config_schema(
|
|
508
|
-
&self,
|
|
509
|
-
view_config: &ViewConfig,
|
|
510
|
-
) -> ApiResult<ColumnConfigSchema> {
|
|
511
|
-
let plugin = self.get_active_plugin()?;
|
|
512
|
-
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
513
|
-
let raw = plugin._plugin_config_schema(&view_config_js)?;
|
|
514
|
-
serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
|
|
393
|
+
Ok(())
|
|
515
394
|
}
|
|
516
395
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
/// restore-prep snapshot (materialize-on-read).
|
|
520
|
-
///
|
|
521
|
-
/// Reads the cached `ColumnStats` (cleared on `view_config_changed`)
|
|
522
|
-
/// so plugins emit gradient defaults against the column's current
|
|
523
|
-
/// `abs_max` instead of falling back to `0`.
|
|
524
|
-
/// [`Self::all_columns_configs_materialized`] warms the cache on
|
|
525
|
-
/// demand before materializing `include: true` Number fields, so
|
|
526
|
-
/// the restore path always observes a real default; sync callers
|
|
527
|
-
/// (column-config strip-on-write) may still see a missing stats
|
|
528
|
-
/// pass-through and the plugin's `?? 0` fallback, but those writes
|
|
529
|
-
/// re-strip on the next render cycle.
|
|
530
|
-
fn query_column_config_schema(
|
|
531
|
-
&self,
|
|
532
|
-
view_config: &ViewConfig,
|
|
533
|
-
session: &Session,
|
|
534
|
-
column_name: &str,
|
|
535
|
-
current_value: Option<&serde_json::Map<String, serde_json::Value>>,
|
|
536
|
-
) -> ApiResult<ColumnConfigSchema> {
|
|
537
|
-
let plugin = self.get_active_plugin()?;
|
|
538
|
-
let plugin_config = self.metadata();
|
|
539
|
-
let names = &plugin_config.config_column_names;
|
|
540
|
-
let group = view_config
|
|
541
|
-
.columns
|
|
542
|
-
.iter()
|
|
543
|
-
.position(|maybe_s| maybe_s.as_deref() == Some(column_name))
|
|
544
|
-
.and_then(|idx| names.get(idx))
|
|
545
|
-
.map(|s| s.as_str());
|
|
546
|
-
|
|
547
|
-
// If the view schema itself hasn't been built yet (e.g. restore
|
|
548
|
-
// landed before `create_view` populated `view_schema`), refuse
|
|
549
|
-
// to answer rather than returning an empty schema. The strip
|
|
550
|
-
// pass in `update_columns_configs` treats `Err` as "leave the
|
|
551
|
-
// incoming cfg untouched"; an empty schema, by contrast, would
|
|
552
|
-
// cause `cfg.retain(|k, _| active.contains(k))` to drop every
|
|
553
|
-
// key and zero out user-supplied column config.
|
|
554
|
-
if !session.metadata().has_view_schema() {
|
|
555
|
-
return Err(JsValue::from("view_schema not initialized").into());
|
|
556
|
-
}
|
|
557
|
-
let Some(view_type) = session.metadata().get_column_view_type(column_name) else {
|
|
558
|
-
return Ok(ColumnConfigSchema { fields: vec![] });
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
let current_js = JsValue::from_serde_ext(¤t_value).unwrap_or(JsValue::NULL);
|
|
562
|
-
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
563
|
-
|
|
564
|
-
// Pull the column's cached stats from the session. The StyleTab
|
|
565
|
-
// pre-warms this via `fetch_column_abs_max` whenever the user
|
|
566
|
-
// opens column settings; the cache is invalidated on every
|
|
567
|
-
// `view_config_changed`, so freshness is bounded.
|
|
568
|
-
let stats = session.get_column_stats(column_name).unwrap_or_default();
|
|
569
|
-
let stats_json = serde_json::json!({
|
|
570
|
-
"abs_max": stats.abs_max,
|
|
571
|
-
});
|
|
572
|
-
let stats_js = JsValue::from_serde_ext(&stats_json).unwrap_or(JsValue::NULL);
|
|
573
|
-
|
|
574
|
-
let raw = plugin._column_config_schema(
|
|
575
|
-
&view_type.to_string(),
|
|
576
|
-
group,
|
|
577
|
-
column_name,
|
|
578
|
-
¤t_js,
|
|
579
|
-
&view_config_js,
|
|
580
|
-
&stats_js,
|
|
581
|
-
)?;
|
|
582
|
-
|
|
583
|
-
serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
|
|
396
|
+
pub fn metadata(&self) -> Rc<PluginStaticConfig> {
|
|
397
|
+
self.borrow().metadata.clone()
|
|
584
398
|
}
|
|
585
399
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
/// treated as "reset this key" — the corresponding bucket entry
|
|
589
|
-
/// is cleared rather than the default being stored literally.
|
|
590
|
-
/// Keys absent from the incoming map are left alone (merge
|
|
591
|
-
/// semantics for the non-default subset).
|
|
592
|
-
pub fn update_plugin_config(
|
|
593
|
-
&self,
|
|
594
|
-
view_config: &ViewConfig,
|
|
595
|
-
update: PluginConfigUpdate,
|
|
596
|
-
) -> bool {
|
|
597
|
-
let Some(n) = self.active_plugin_name() else {
|
|
598
|
-
return false;
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
let schema = self.query_plugin_config_schema(view_config).ok();
|
|
602
|
-
let mut st = self.borrow_mut();
|
|
603
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
604
|
-
match update {
|
|
605
|
-
OptionalUpdate::SetDefault => {
|
|
606
|
-
let changed = !bucket.plugin.is_empty();
|
|
607
|
-
bucket.plugin.clear();
|
|
608
|
-
changed
|
|
609
|
-
},
|
|
610
|
-
OptionalUpdate::Missing => false,
|
|
611
|
-
OptionalUpdate::Update(mut map) => {
|
|
612
|
-
let mut changed = false;
|
|
613
|
-
if let Some(s) = &schema {
|
|
614
|
-
let active = s.active_keys();
|
|
615
|
-
map.retain(|k, _| active.contains(k));
|
|
616
|
-
// Default-valued entries in a restore payload
|
|
617
|
-
// semantically reset the key — strip from the
|
|
618
|
-
// map AND clear any existing override in the
|
|
619
|
-
// bucket so the wholesale-restore path matches
|
|
620
|
-
// the live-edit path (where the widget emits an
|
|
621
|
-
// empty value to clear).
|
|
622
|
-
map.retain(|key, value| {
|
|
623
|
-
let is_default = s
|
|
624
|
-
.fields
|
|
625
|
-
.iter()
|
|
626
|
-
.any(|spec| matches_declared_default(spec, key, value));
|
|
627
|
-
if is_default {
|
|
628
|
-
if bucket.plugin.remove(key).is_some() {
|
|
629
|
-
changed = true;
|
|
630
|
-
}
|
|
631
|
-
false
|
|
632
|
-
} else {
|
|
633
|
-
true
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
for (k, v) in map {
|
|
639
|
-
let prev = bucket.plugin.insert(k, v.clone());
|
|
640
|
-
if prev.as_ref() != Some(&v) {
|
|
641
|
-
changed = true;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
changed
|
|
646
|
-
},
|
|
647
|
-
}
|
|
400
|
+
pub fn is_chart(&self) -> bool {
|
|
401
|
+
self.metadata().name.as_str() != "Datagrid"
|
|
648
402
|
}
|
|
649
403
|
|
|
650
|
-
///
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
/// stripped before applying so default picks reset the key
|
|
655
|
-
/// rather than store the default literally.
|
|
656
|
-
pub fn update_plugin_config_field(
|
|
657
|
-
&self,
|
|
658
|
-
view_config: &ViewConfig,
|
|
659
|
-
mut update: ColumnConfigFieldUpdate,
|
|
660
|
-
) -> bool {
|
|
661
|
-
let Some(n) = self.active_plugin_name() else {
|
|
662
|
-
return false;
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
if let Ok(schema) = self.query_plugin_config_schema(view_config) {
|
|
666
|
-
strip_default_values(&schema, &mut update.value);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
let mut st = self.borrow_mut();
|
|
670
|
-
let bucket = st.plugin_states.entry(n).or_default();
|
|
671
|
-
let mut changed = false;
|
|
404
|
+
/// Whether the active plugin opts into per-column style controls.
|
|
405
|
+
pub fn can_render_column_styles(&self) -> bool {
|
|
406
|
+
self.metadata().can_render_column_styles
|
|
407
|
+
}
|
|
672
408
|
|
|
673
|
-
|
|
674
|
-
if let Some(v) = update.value.get(k) {
|
|
675
|
-
let prev = bucket.plugin.insert(k.to_string(), v.clone());
|
|
676
|
-
if prev.as_ref() != Some(v) {
|
|
677
|
-
changed = true;
|
|
678
|
-
}
|
|
679
|
-
} else if bucket.plugin.remove(k).is_some() {
|
|
680
|
-
changed = true;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
409
|
+
// ─── Per-column config (active plugin's bucket) ───────────────────
|
|
683
410
|
|
|
684
|
-
|
|
685
|
-
}
|
|
411
|
+
// ─── Plugin-level config (active plugin's bucket) ─────────────────
|
|
686
412
|
|
|
687
413
|
/// Whether the active plugin's render warning is currently armed
|
|
688
414
|
/// (i.e. an oversized view will be capped). Becomes `false` once
|
|
@@ -710,18 +436,31 @@ impl Renderer {
|
|
|
710
436
|
self.0.borrow_mut().plugin_store.plugin_configs().clone()
|
|
711
437
|
}
|
|
712
438
|
|
|
713
|
-
///
|
|
714
|
-
///
|
|
715
|
-
///
|
|
716
|
-
///
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
439
|
+
/// The currently-selected plugin, or `None` if none has been selected yet.
|
|
440
|
+
///
|
|
441
|
+
/// This is a PURE QUERY (command-query separation): unlike
|
|
442
|
+
/// [`Self::ensure_plugin_selected`] it never selects a default plugin and
|
|
443
|
+
/// never triggers the lazy `PluginStore` snapshot before a plugin exists.
|
|
444
|
+
/// Pre-draw / render-path code MUST use this and handle `None` — selecting
|
|
445
|
+
/// a plugin on the first render, before the real plugins register,
|
|
446
|
+
/// permanently pins the per-renderer store to the built-in `Debug`
|
|
447
|
+
/// (init-order race, surfaces on Safari).
|
|
448
|
+
pub fn active_plugin(&self) -> Option<JsPerspectiveViewerPlugin> {
|
|
449
|
+
// Bail on `plugins_idx` BEFORE touching `plugin_store`, so an unselected
|
|
450
|
+
// renderer never snapshots the registry.
|
|
451
|
+
let idx = self.0.borrow().plugins_idx?;
|
|
452
|
+
self.0.borrow_mut().plugin_store.plugins().get(idx).cloned()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/// Ensure a plugin is selected — the registry default when nothing has
|
|
456
|
+
/// ever been selected — and return it. The COMMAND half of
|
|
457
|
+
/// [`Self::active_plugin`]: this is the only path that selects a default and
|
|
458
|
+
/// snapshots the store, so it must run only when actually drawing (by which
|
|
459
|
+
/// point real plugins have registered). Errors if no plugins are
|
|
460
|
+
/// registered.
|
|
461
|
+
pub fn ensure_plugin_selected(&self) -> ApiResult<JsPerspectiveViewerPlugin> {
|
|
462
|
+
self.commit_plugin(None)?;
|
|
463
|
+
Ok(self.active_plugin().ok_or("No Plugin")?)
|
|
725
464
|
}
|
|
726
465
|
|
|
727
466
|
/// Gets a specific `JsPerspectiveViewerPlugin` by name.
|
|
@@ -735,25 +474,42 @@ impl Renderer {
|
|
|
735
474
|
Ok(result.unwrap())
|
|
736
475
|
}
|
|
737
476
|
|
|
477
|
+
/// Whether the active plugin has completed a draw. A query, not a
|
|
478
|
+
/// command — and an explicit flag rather than the old `is_connected()`
|
|
479
|
+
/// inference, which eager plugin mounting (an element in the DOM before
|
|
480
|
+
/// its first draw) would falsify.
|
|
738
481
|
pub fn is_plugin_activated(&self) -> ApiResult<bool> {
|
|
739
|
-
Ok(self
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
482
|
+
Ok(self.0.has_drawn.get())
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/// Mount the selected plugin element into the viewer's light DOM without
|
|
486
|
+
/// drawing it (see [`activate::mount_plugin`] — idempotent). Pure query:
|
|
487
|
+
/// a renderer with no selection is a no-op, deferring to the draw path's
|
|
488
|
+
/// `ensure_plugin_selected`. Used to mount eagerly — at panel creation
|
|
489
|
+
/// and at locked-draw start — so a slow first view query never leaves an
|
|
490
|
+
/// empty panel frame. NOTE eager mounting is exactly why
|
|
491
|
+
/// [`Self::is_plugin_activated`] must be an explicit has-drawn flag, not
|
|
492
|
+
/// DOM-connectedness.
|
|
493
|
+
pub fn mount_active_plugin(&self) -> ApiResult<()> {
|
|
494
|
+
if let Some(plugin) = self.active_plugin() {
|
|
495
|
+
let viewer_elem = self.0.borrow().viewer_elem.clone();
|
|
496
|
+
mount_plugin(&viewer_elem, &plugin, self.slot_name().as_deref())?;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
Ok(())
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// Record whether this panel is the active panel (data only — see the
|
|
503
|
+
/// field doc; the CSS class lands at the next locked plugin dispatch).
|
|
504
|
+
pub fn set_active_flag(&self, is_active: bool) {
|
|
505
|
+
self.0.is_active_panel.set(is_active);
|
|
743
506
|
}
|
|
744
507
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
get_row_and_col_limits(view, &meta, self.is_render_warning_enabled()).await?;
|
|
751
|
-
limits.is_update = false;
|
|
752
|
-
plugin
|
|
753
|
-
.draw(view.clone().into(), limits.max_cols, limits.max_rows, false)
|
|
754
|
-
.await?;
|
|
755
|
-
|
|
756
|
-
Ok(JsValue::UNDEFINED)
|
|
508
|
+
/// Record whether this panel is the workspace's only panel (data only —
|
|
509
|
+
/// see the field doc; the CSS class lands at the next locked plugin
|
|
510
|
+
/// dispatch).
|
|
511
|
+
pub fn set_solo_flag(&self, is_solo: bool) {
|
|
512
|
+
self.0.is_solo_panel.set(is_solo);
|
|
757
513
|
}
|
|
758
514
|
|
|
759
515
|
pub fn set_throttle(&self, val: Option<f64>) {
|
|
@@ -777,10 +533,22 @@ impl Renderer {
|
|
|
777
533
|
self.0.render_warning.set(false);
|
|
778
534
|
}
|
|
779
535
|
|
|
780
|
-
|
|
536
|
+
/// Resolve a [`PluginUpdate`] against the current selection. Returns the
|
|
537
|
+
/// target plugin's index + static config when the update names a plugin
|
|
538
|
+
/// (or the registry default, for `SetDefault`) that differs from the
|
|
539
|
+
/// current selection; `None` when the update is `Missing`, names an
|
|
540
|
+
/// unknown plugin, or resolves to the already-active one.
|
|
541
|
+
///
|
|
542
|
+
/// PURE query — nothing is staged on the `Renderer`. Thread the returned
|
|
543
|
+
/// index *into* a locked draw task and commit it there with
|
|
544
|
+
/// [`Self::commit_plugin`]: plugin-swap intent must never exist outside a
|
|
545
|
+
/// running draw transaction, or an unrelated draw that wins the lock first
|
|
546
|
+
/// (e.g. a `table_updated` redraw during a `restore()`) could observe or
|
|
547
|
+
/// commit a half-applied swap.
|
|
548
|
+
pub fn resolve_plugin_update(
|
|
781
549
|
&self,
|
|
782
550
|
update: &PluginUpdate,
|
|
783
|
-
) -> Option<Rc<PluginStaticConfig
|
|
551
|
+
) -> Option<(usize, Rc<PluginStaticConfig>)> {
|
|
784
552
|
let default_plugin_name = PLUGIN_REGISTRY.default_plugin_name();
|
|
785
553
|
let name = match update {
|
|
786
554
|
PluginUpdate::Missing => return None,
|
|
@@ -795,47 +563,43 @@ impl Renderer {
|
|
|
795
563
|
);
|
|
796
564
|
|
|
797
565
|
if changed {
|
|
798
|
-
|
|
799
|
-
|
|
566
|
+
let config = self
|
|
567
|
+
.0
|
|
800
568
|
.borrow_mut()
|
|
801
569
|
.plugin_store
|
|
802
570
|
.plugin_configs()
|
|
803
571
|
.get(idx)
|
|
804
|
-
.cloned()
|
|
572
|
+
.cloned()?;
|
|
573
|
+
Some((idx, config))
|
|
805
574
|
} else {
|
|
806
575
|
None
|
|
807
576
|
}
|
|
808
577
|
}
|
|
809
578
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
579
|
+
/// Commit a plugin selection previously resolved by
|
|
580
|
+
/// [`Self::resolve_plugin_update`]. COMMAND — call only from inside a
|
|
581
|
+
/// locked draw task, so the swap lands atomically with the view rebuild
|
|
582
|
+
/// and draw it belongs to. `None` ensures *some* plugin is selected (the
|
|
583
|
+
/// registry default) without changing an existing selection. Returns
|
|
584
|
+
/// whether the active plugin changed (`None`'s default-selection case
|
|
585
|
+
/// reports `false`, preserving first-selection semantics for the
|
|
586
|
+
/// swap-restore pass in the draw tasks).
|
|
587
|
+
pub fn commit_plugin(&self, idx: Option<usize>) -> ApiResult<bool> {
|
|
588
|
+
let idx = match idx {
|
|
589
|
+
Some(idx) => idx,
|
|
590
|
+
None => {
|
|
591
|
+
if self.0.borrow().plugins_idx.is_none() {
|
|
592
|
+
let name = PLUGIN_REGISTRY.default_plugin_name();
|
|
593
|
+
let idx = self
|
|
594
|
+
.find_plugin_idx(&name)
|
|
595
|
+
.ok_or_else(|| JsValue::from(format!("Unknown plugin '{name}'")))?;
|
|
596
|
+
|
|
597
|
+
self.commit_plugin_idx(idx)?;
|
|
598
|
+
}
|
|
831
599
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
let name = name.unwrap_or(default_plugin_name.as_str());
|
|
836
|
-
let idx = self
|
|
837
|
-
.find_plugin_idx(name)
|
|
838
|
-
.ok_or_else(|| JsValue::from(format!("Unknown plugin '{name}'")))?;
|
|
600
|
+
return Ok(false);
|
|
601
|
+
},
|
|
602
|
+
};
|
|
839
603
|
|
|
840
604
|
let changed = !matches!(
|
|
841
605
|
self.0.borrow().plugins_idx,
|
|
@@ -849,11 +613,16 @@ impl Renderer {
|
|
|
849
613
|
Ok(changed)
|
|
850
614
|
}
|
|
851
615
|
|
|
852
|
-
/// Shared tail of `
|
|
616
|
+
/// Shared tail of [`Self::commit_plugin`]: switch the
|
|
853
617
|
/// active plugin to `idx`, swap in its cached `PluginStaticConfig`,
|
|
854
618
|
/// reset the per-plugin render-warning flag, and fire
|
|
855
619
|
/// `plugin_changed`.
|
|
856
620
|
fn commit_plugin_idx(&self, idx: usize) -> ApiResult<()> {
|
|
621
|
+
// The newly-selected plugin element has not drawn (a swap keeps the
|
|
622
|
+
// OLD plugin mounted until the new one's draw lands) — and has
|
|
623
|
+
// captured no CSS.
|
|
624
|
+
self.0.has_drawn.set(false);
|
|
625
|
+
self.0.captured_theme.borrow_mut().take();
|
|
857
626
|
self.borrow_mut().plugins_idx = Some(idx);
|
|
858
627
|
let config = self
|
|
859
628
|
.0
|
|
@@ -866,7 +635,10 @@ impl Renderer {
|
|
|
866
635
|
|
|
867
636
|
self.borrow_mut().metadata = config.clone();
|
|
868
637
|
self.0.render_warning.set(true);
|
|
869
|
-
|
|
638
|
+
// `commit_plugin_idx` is called *by* the selection path, so it must use
|
|
639
|
+
// the pure query (never `ensure_plugin_selected`, which would recurse
|
|
640
|
+
// through `commit_plugin`). `plugins_idx` was just set above.
|
|
641
|
+
let plugin: JsPerspectiveViewerPlugin = self.active_plugin().ok_or("No Plugin")?;
|
|
870
642
|
|
|
871
643
|
// Push the newly-activated plugin's stored bucket through
|
|
872
644
|
// `plugin.restore` so the swap immediately reflects any
|
|
@@ -886,192 +658,6 @@ impl Renderer {
|
|
|
886
658
|
Ok(())
|
|
887
659
|
}
|
|
888
660
|
|
|
889
|
-
pub async fn with_lock<T>(self, task: impl Future<Output = ApiResult<T>>) -> ApiResult<T> {
|
|
890
|
-
let draw_mutex = self.draw_lock();
|
|
891
|
-
draw_mutex.lock(task).await
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
pub async fn resize(&self) -> ApiResult<()> {
|
|
895
|
-
let draw_mutex = self.draw_lock();
|
|
896
|
-
let timer = self.render_timer();
|
|
897
|
-
draw_mutex
|
|
898
|
-
.debounce(async {
|
|
899
|
-
set_timeout(timer.get_throttle()).await?;
|
|
900
|
-
let jsplugin = self.get_active_plugin()?;
|
|
901
|
-
jsplugin.resize().await?;
|
|
902
|
-
Ok(())
|
|
903
|
-
})
|
|
904
|
-
.await
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
pub async fn resize_with_dimensions(&self, width: f64, height: f64) -> ApiResult<()> {
|
|
908
|
-
let draw_mutex = self.draw_lock();
|
|
909
|
-
let timer = self.render_timer();
|
|
910
|
-
draw_mutex
|
|
911
|
-
.debounce(async {
|
|
912
|
-
set_timeout(timer.get_throttle()).await?;
|
|
913
|
-
let plugin = self.get_active_plugin()?;
|
|
914
|
-
let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
|
|
915
|
-
let rect = main_panel.get_bounding_client_rect();
|
|
916
|
-
if (height - rect.height()).abs() > 0.5 || (width - rect.width()).abs() > 0.5 {
|
|
917
|
-
let new_width = format!("{}px", width);
|
|
918
|
-
let new_height = format!("{}px", height);
|
|
919
|
-
main_panel.style().set_property("width", &new_width)?;
|
|
920
|
-
main_panel.style().set_property("height", &new_height)?;
|
|
921
|
-
let result = plugin.resize().await;
|
|
922
|
-
main_panel.style().set_property("width", "")?;
|
|
923
|
-
main_panel.style().set_property("height", "")?;
|
|
924
|
-
result?;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
Ok(())
|
|
928
|
-
})
|
|
929
|
-
.await
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/// This will take a future which _should_ create a new view and then will
|
|
933
|
-
/// draw it. As the `session` closure is asynchronous, it can be cancelled
|
|
934
|
-
/// by returning `None`.
|
|
935
|
-
pub async fn draw(
|
|
936
|
-
&self,
|
|
937
|
-
session: impl Future<Output = ApiResult<Option<View>>>,
|
|
938
|
-
) -> ApiResult<()> {
|
|
939
|
-
self.draw_plugin(session, false).await
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/// This will update an already existing view
|
|
943
|
-
pub async fn update(&self, session: Option<View>) -> ApiResult<()> {
|
|
944
|
-
self.draw_plugin(async { Ok(session) }, true).await
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
async fn draw_plugin(
|
|
948
|
-
&self,
|
|
949
|
-
session: impl Future<Output = ApiResult<Option<View>>>,
|
|
950
|
-
is_update: bool,
|
|
951
|
-
) -> ApiResult<()> {
|
|
952
|
-
let timer = self.render_timer();
|
|
953
|
-
let task = async move {
|
|
954
|
-
if is_update {
|
|
955
|
-
set_timeout(timer.get_throttle()).await?;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if let Some(view) = session.await? {
|
|
959
|
-
timer.capture_time(self.draw_view(&view, is_update)).await
|
|
960
|
-
} else {
|
|
961
|
-
tracing::debug!("Render skipped, no `View` attached");
|
|
962
|
-
Ok(())
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
let draw_mutex = self.draw_lock();
|
|
967
|
-
if is_update {
|
|
968
|
-
draw_mutex.debounce(task).await
|
|
969
|
-
} else {
|
|
970
|
-
draw_mutex.lock(task).await
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
async fn draw_view(&self, view: &perspective_client::View, is_update: bool) -> ApiResult<()> {
|
|
975
|
-
let plugin = self.get_active_plugin()?;
|
|
976
|
-
let meta = self.metadata();
|
|
977
|
-
let mut limits =
|
|
978
|
-
get_row_and_col_limits(view, &meta, self.is_render_warning_enabled()).await?;
|
|
979
|
-
limits.is_update = is_update;
|
|
980
|
-
if let Some(cb) = self.0.on_render_limits_changed.borrow().as_ref() {
|
|
981
|
-
cb.emit(limits);
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
let viewer_elem = &self.0.borrow().viewer_elem.clone();
|
|
985
|
-
let result = if is_update {
|
|
986
|
-
let task = plugin.update(view.clone().into(), limits.max_cols, limits.max_rows, false);
|
|
987
|
-
activate_plugin(viewer_elem, &plugin, task).await
|
|
988
|
-
} else {
|
|
989
|
-
let task = plugin.draw(view.clone().into(), limits.max_cols, limits.max_rows, false);
|
|
990
|
-
activate_plugin(viewer_elem, &plugin, task).await
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
if let Err(error) = result.ignore_view_delete() {
|
|
994
|
-
tracing::warn!("{}", error);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
remove_inactive_plugin(
|
|
998
|
-
viewer_elem,
|
|
999
|
-
&plugin,
|
|
1000
|
-
self.plugin_data.borrow_mut().plugin_store.plugins(),
|
|
1001
|
-
)
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
/// Decide whether to draw plugin or self first based on whether the panel
|
|
1005
|
-
/// is opening or closing, then draw with a timeout. If the timeout
|
|
1006
|
-
/// triggers, draw self and resolve `on_toggle` but still await the
|
|
1007
|
-
/// completion of the draw task.
|
|
1008
|
-
pub async fn presize(
|
|
1009
|
-
&self,
|
|
1010
|
-
open: bool,
|
|
1011
|
-
panel_task: impl Future<Output = ApiResult<()>>,
|
|
1012
|
-
) -> ApiResult<JsValue> {
|
|
1013
|
-
let render_task = self.resize_with_timeout(open);
|
|
1014
|
-
let result = if open {
|
|
1015
|
-
panel_task.await?;
|
|
1016
|
-
render_task.await
|
|
1017
|
-
} else {
|
|
1018
|
-
let result = render_task.await;
|
|
1019
|
-
panel_task.await?;
|
|
1020
|
-
result
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
match result {
|
|
1024
|
-
Ok(x) => x,
|
|
1025
|
-
Err(cont) => {
|
|
1026
|
-
tracing::warn!("Presize took longer than {}ms", PRESIZE_TIMEOUT);
|
|
1027
|
-
cont.await.unwrap()
|
|
1028
|
-
},
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
/// Lock on `resize()` task, in parallel with a timeout. In the return
|
|
1033
|
-
/// type, `Result::Err` contains the continuation task, which must be
|
|
1034
|
-
/// awaited lest the plugin draw itself never trigger.
|
|
1035
|
-
async fn resize_with_timeout(&self, open: bool) -> Result<TaskResult, TimeoutTask<'_>> {
|
|
1036
|
-
let task = async move {
|
|
1037
|
-
if open {
|
|
1038
|
-
self.get_active_plugin()?.resize().await
|
|
1039
|
-
} else {
|
|
1040
|
-
self.resize_with_explicit_dimensions().await
|
|
1041
|
-
}
|
|
1042
|
-
};
|
|
1043
|
-
|
|
1044
|
-
let draw_lock = self.draw_lock();
|
|
1045
|
-
let tasks: [TimeoutTask<'_>; 2] = [
|
|
1046
|
-
Box::pin(async move { Some(draw_lock.lock(task).await) }),
|
|
1047
|
-
Box::pin(async {
|
|
1048
|
-
set_timeout(PRESIZE_TIMEOUT).await.unwrap();
|
|
1049
|
-
None
|
|
1050
|
-
}),
|
|
1051
|
-
];
|
|
1052
|
-
|
|
1053
|
-
let (x, _, y) = select_all(tasks).await;
|
|
1054
|
-
x.ok_or_else(|| y.into_iter().next().unwrap())
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/// Resize the `<div>` offscreen, then resize the plugin
|
|
1058
|
-
async fn resize_with_explicit_dimensions(&self) -> TaskResult {
|
|
1059
|
-
let plugin = self.get_active_plugin()?;
|
|
1060
|
-
let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
|
|
1061
|
-
let new_width = format!("{}px", self.0.borrow().viewer_elem.client_width());
|
|
1062
|
-
let new_height = format!("{}px", self.0.borrow().viewer_elem.client_height());
|
|
1063
|
-
main_panel.style().set_property("width", &new_width)?;
|
|
1064
|
-
main_panel.style().set_property("height", &new_height)?;
|
|
1065
|
-
let result = plugin.resize().await;
|
|
1066
|
-
main_panel.style().set_property("width", "")?;
|
|
1067
|
-
main_panel.style().set_property("height", "")?;
|
|
1068
|
-
result
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
fn draw_lock(&self) -> DebounceMutex {
|
|
1072
|
-
self.draw_lock.clone()
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
661
|
pub fn render_timer(&self) -> MovingWindowRenderTimer {
|
|
1076
662
|
self.0.borrow().timer.clone()
|
|
1077
663
|
}
|
|
@@ -1080,10 +666,6 @@ impl Renderer {
|
|
|
1080
666
|
let short_name = make_short_name(name);
|
|
1081
667
|
let mut borrowed = self.0.borrow_mut();
|
|
1082
668
|
let configs = borrowed.plugin_store.plugin_configs();
|
|
1083
|
-
// Prefer an exact (normalised) match so e.g. `"Y Line"` doesn't
|
|
1084
|
-
// substring-resolve to `"X/Y Line"` just because it was registered
|
|
1085
|
-
// first. Falls back to `contains` so short/abbreviated names
|
|
1086
|
-
// (`restore({ plugin: "scat" })` → `"scatter"`) still work.
|
|
1087
669
|
let short_names: Vec<String> = configs.iter().map(|c| make_short_name(&c.name)).collect();
|
|
1088
670
|
if let Some(i) = short_names.iter().position(|n| n == &short_name) {
|
|
1089
671
|
return Some(i);
|
|
@@ -1107,14 +689,7 @@ impl Renderer {
|
|
|
1107
689
|
/// suitable for passing as a Yew prop. Called by the root component
|
|
1108
690
|
/// whenever a renderer-related PubSub event fires.
|
|
1109
691
|
pub fn to_props(&self, render_limits: Option<RenderLimits>) -> RendererProps {
|
|
1110
|
-
|
|
1111
|
-
// selected yet. Calling `get_active_plugin()` or `get_all_plugins()`
|
|
1112
|
-
// triggers `PluginStore::init_lazy()`, which snapshots the
|
|
1113
|
-
// PLUGIN_REGISTRY. If this happens during component `create()` —
|
|
1114
|
-
// before JavaScript has called `registerPlugin()` — the cache will
|
|
1115
|
-
// only contain the default Debug plugin and custom plugins registered
|
|
1116
|
-
// later will never be found.
|
|
1117
|
-
let has_plugin = self.0.borrow().plugins_idx.is_some();
|
|
692
|
+
let has_plugin = self.active_plugin().is_some();
|
|
1118
693
|
if has_plugin {
|
|
1119
694
|
let config = self.metadata();
|
|
1120
695
|
let plugin_name = Some(config.name.clone());
|
|
@@ -1125,8 +700,8 @@ impl Renderer {
|
|
|
1125
700
|
.map(|c| c.name.clone())
|
|
1126
701
|
.collect::<Vec<_>>()
|
|
1127
702
|
.into();
|
|
1128
|
-
let plugin_config = PtrEqRc::new(self.get_plugin_config());
|
|
1129
703
|
|
|
704
|
+
let plugin_config = PtrEqRc::new(self.get_plugin_config());
|
|
1130
705
|
RendererProps {
|
|
1131
706
|
plugin_name,
|
|
1132
707
|
config,
|
|
@@ -1147,65 +722,3 @@ impl Renderer {
|
|
|
1147
722
|
}
|
|
1148
723
|
}
|
|
1149
724
|
}
|
|
1150
|
-
|
|
1151
|
-
/// Drop entries from `map` whose value matches the schema-declared
|
|
1152
|
-
/// default for that key. Used by both the plugin-config and
|
|
1153
|
-
/// columns-config write paths to converge buckets to the
|
|
1154
|
-
/// "empty ⇒ reads-default" invariant. For `ControlSpec::Number`
|
|
1155
|
-
/// entries marked `include: Some(true)`,
|
|
1156
|
-
/// [`matches_declared_default`] short-circuits so the value survives
|
|
1157
|
-
/// (used when the declared default is data-dependent and unreliable —
|
|
1158
|
-
/// e.g. Datagrid's `fg_gradient`, whose default is the column's
|
|
1159
|
-
/// `abs_max`).
|
|
1160
|
-
fn strip_default_values(
|
|
1161
|
-
schema: &ColumnConfigSchema,
|
|
1162
|
-
map: &mut serde_json::Map<String, serde_json::Value>,
|
|
1163
|
-
) {
|
|
1164
|
-
map.retain(|key, value| {
|
|
1165
|
-
!schema
|
|
1166
|
-
.fields
|
|
1167
|
-
.iter()
|
|
1168
|
-
.any(|spec| matches_declared_default(spec, key, value))
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
/// Does `value` for `key` match the `default` declared by `spec`?
|
|
1173
|
-
/// Composite variants (`NumberSeriesStyle`, `DatetimeFormat`, etc.) own
|
|
1174
|
-
/// nested defaults that don't have a single comparable scalar — the
|
|
1175
|
-
/// widget is responsible for emitting empty values when the user
|
|
1176
|
-
/// resets composite controls, so this helper returns `false` for them.
|
|
1177
|
-
fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> bool {
|
|
1178
|
-
match spec {
|
|
1179
|
-
ControlSpec::Enum {
|
|
1180
|
-
key: k, default, ..
|
|
1181
|
-
} if k == key => value.as_str() == Some(default.as_str()),
|
|
1182
|
-
ControlSpec::Bool {
|
|
1183
|
-
key: k, default, ..
|
|
1184
|
-
} if k == key => value.as_bool() == Some(*default),
|
|
1185
|
-
ControlSpec::Number {
|
|
1186
|
-
key: k,
|
|
1187
|
-
include: Some(true),
|
|
1188
|
-
..
|
|
1189
|
-
} if k == key => false,
|
|
1190
|
-
ControlSpec::Number {
|
|
1191
|
-
key: k, default, ..
|
|
1192
|
-
} if k == key => value.as_f64() == Some(*default),
|
|
1193
|
-
ControlSpec::String {
|
|
1194
|
-
key: k, default, ..
|
|
1195
|
-
} if k == key => value.as_str() == Some(default.as_str()),
|
|
1196
|
-
ControlSpec::Color {
|
|
1197
|
-
key: k, default, ..
|
|
1198
|
-
} if k == key => value.as_str() == Some(default.as_str()),
|
|
1199
|
-
ControlSpec::ColorRange {
|
|
1200
|
-
key_pos,
|
|
1201
|
-
default_pos,
|
|
1202
|
-
..
|
|
1203
|
-
} if key_pos == key => value.as_str() == Some(default_pos.as_str()),
|
|
1204
|
-
ControlSpec::ColorRange {
|
|
1205
|
-
key_neg,
|
|
1206
|
-
default_neg,
|
|
1207
|
-
..
|
|
1208
|
-
} if key_neg == key => value.as_str() == Some(default_neg.as_str()),
|
|
1209
|
-
_ => false,
|
|
1210
|
-
}
|
|
1211
|
-
}
|