@perspective-dev/viewer 4.5.2 → 5.1.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/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/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 +41 -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 +8 -2
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
- package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- 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 +299 -78
- package/dist/wasm/perspective-viewer.js +544 -186
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-selector.css +1 -0
- package/src/css/column-settings-panel.css +68 -3
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +22 -1
- package/src/css/containers/context-menu.css +107 -0
- package/src/css/containers/tabs.css +2 -2
- 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 +188 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +28 -45
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +40 -82
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +11 -9
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +25 -14
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +73 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +181 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/editable_header.rs +2 -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 +316 -71
- package/src/rust/components/mod.rs +5 -0
- package/src/rust/components/panel_menu.rs +493 -0
- package/src/rust/components/panel_tab.rs +510 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/portal.rs +7 -0
- 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 +182 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +255 -0
- package/src/rust/components/viewer/settings.rs +434 -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 +245 -756
- package/src/rust/components/window_editor.rs +1013 -0
- 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 +24 -2
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/columns_iter_set.rs +7 -1
- 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 +463 -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 +384 -838
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +66 -9
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +755 -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/edit_window.rs +155 -0
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +30 -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/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +189 -17
- 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/svg/pin-icon.svg +5 -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 +5 -2
- package/src/themes/intl/de.css +7 -0
- package/src/themes/intl/es.css +7 -0
- package/src/themes/intl/fr.css +7 -0
- package/src/themes/intl/ja.css +7 -0
- package/src/themes/intl/pt.css +7 -0
- package/src/themes/intl/zh.css +7 -0
- package/src/themes/intl.css +10 -1
- 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 +16 -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 +7 -2
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
- package/src/ts/ts-rs/WindowAggregate.ts +3 -0
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +46 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- 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-8a8c7d5d696b43ae/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/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,59 @@ 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
|
-
#[derive(Clone,
|
|
69
|
-
pub
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
/// One queued geometry task for the [`RendererData::geometry_slot`] runner.
|
|
57
|
+
/// `Measure` re-measures the plugin's live box (reactive resize); `Presize`
|
|
58
|
+
/// renders an anticipated box ahead of a layout commit. A later command
|
|
59
|
+
/// subsumes an earlier unconsumed one: `Measure` reflects whatever geometry
|
|
60
|
+
/// has committed by run time, and `Presize` anticipates the NEXT commit —
|
|
61
|
+
/// callers issue them in commit order.
|
|
62
|
+
#[derive(Clone, Copy)]
|
|
63
|
+
pub(crate) enum GeometryCmd {
|
|
64
|
+
Measure,
|
|
65
|
+
Presize {
|
|
66
|
+
translate: Option<(f64, f64)>,
|
|
67
|
+
width: f64,
|
|
68
|
+
height: f64,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Everything a plugin may read back during its render, all belonging to
|
|
73
|
+
/// one snapshot (invariant I5). Built once per bound `View` by the render
|
|
74
|
+
/// pipeline and CACHED on the `Renderer`; pinning it for a run is an `Rc`
|
|
75
|
+
/// clone (cheap enough for resize-tick-rate runs). While pinned, the
|
|
76
|
+
/// per-panel element getters (`getViewConfigPanel`, `getTablePanel`,
|
|
77
|
+
/// `getClientPanel`, `getEditPortPanel`) answer from it — the plugin ABI is
|
|
78
|
+
/// inside the snapshot boundary.
|
|
79
|
+
pub struct RenderContext {
|
|
80
|
+
pub view_config: Rc<perspective_client::config::ViewConfig>,
|
|
81
|
+
pub view: perspective_client::View,
|
|
82
|
+
pub table: perspective_client::Table,
|
|
83
|
+
pub client: perspective_client::Client,
|
|
84
|
+
pub edit_port: Option<f64>,
|
|
85
|
+
pub theme: Option<String>,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// RAII pin for a [`RenderContext`] — cleared on drop, INCLUDING error
|
|
89
|
+
/// exits, so a failed run can't leak a stale context.
|
|
90
|
+
pub struct ContextPin(Renderer);
|
|
91
|
+
|
|
92
|
+
impl Drop for ContextPin {
|
|
93
|
+
fn drop(&mut self) {
|
|
94
|
+
self.0.0.active_context.borrow_mut().take();
|
|
95
|
+
}
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
/// Immutable state
|
|
@@ -92,8 +116,98 @@ pub struct RendererData {
|
|
|
92
116
|
/// dismissable.
|
|
93
117
|
pub render_warning: Cell<bool>,
|
|
94
118
|
|
|
119
|
+
/// Count of in-flight [`Renderer::resize_with_dimensions`] /
|
|
120
|
+
/// [`Renderer::presize_with_box`] presize calls.
|
|
121
|
+
presize_pending: Cell<u32>,
|
|
122
|
+
|
|
123
|
+
/// Debounce slot for geometry tasks (`resize` / presize), separate from
|
|
124
|
+
/// the default slot data-update redraws coalesce on. Invariant: a
|
|
125
|
+
/// geometry request must never resolve via a parked DATA task — a
|
|
126
|
+
/// pending update's redraw renders at the worker's current dimensions
|
|
127
|
+
/// and cannot subsume a size change (the drag-during-updates permanent
|
|
128
|
+
/// skew).
|
|
129
|
+
geometry_slot: DebounceSlot,
|
|
130
|
+
|
|
131
|
+
/// Latest-wins parameter cell for the geometry slot, consumed by the
|
|
132
|
+
/// runner AT RUN TIME under the lock. Parameters live here rather than
|
|
133
|
+
/// in runner captures so a parked runner can never act on a stale
|
|
134
|
+
/// target when later calls coalesced onto it.
|
|
135
|
+
geometry_cmd: Cell<Option<GeometryCmd>>,
|
|
136
|
+
|
|
95
137
|
/// Fires after every draw/update with the computed render limits.
|
|
96
138
|
pub on_render_limits_changed: RefCell<Option<Callback<RenderLimits>>>,
|
|
139
|
+
|
|
140
|
+
/// The layout slot name (panel id) under which this renderer mounts its
|
|
141
|
+
/// plugin in the viewer's light DOM, so multiple panels' plugins can
|
|
142
|
+
/// coexist there.
|
|
143
|
+
slot_name: RefCell<Option<String>>,
|
|
144
|
+
|
|
145
|
+
/// This panel's selected theme name (per-panel theming), or `None` to
|
|
146
|
+
/// inherit the element-level (active) theme.
|
|
147
|
+
theme: RefCell<Option<String>>,
|
|
148
|
+
|
|
149
|
+
/// The registry default theme name (first registered), cached here so
|
|
150
|
+
/// LOCKED draw paths can resolve this panel's EFFECTIVE theme
|
|
151
|
+
/// synchronously (`Presentation::get_default_theme_name` is async — it
|
|
152
|
+
/// awaits theme discovery). Seeded by every content-load path before its
|
|
153
|
+
/// first locked draw (`restore_and_render`, `load()`, the
|
|
154
|
+
/// resize-observer's deferred first render) and kept fresh by the root's
|
|
155
|
+
/// `UpdatePresentation` default-theme fan-out and `resetThemes`.
|
|
156
|
+
default_theme: RefCell<Option<String>>,
|
|
157
|
+
|
|
158
|
+
/// Whether the active plugin has completed a draw. An EXPLICIT flag —
|
|
159
|
+
/// not inferred from DOM connectedness — because plugin elements may be
|
|
160
|
+
/// mounted eagerly (at panel creation / draw start, before the view
|
|
161
|
+
/// query resolves), so "in the DOM" no longer implies "has rendered".
|
|
162
|
+
/// Set by a successful `draw_view`; cleared on plugin swap
|
|
163
|
+
/// (`commit_plugin_idx`), `dispose` and `delete`.
|
|
164
|
+
has_drawn: Cell<bool>,
|
|
165
|
+
|
|
166
|
+
/// The effective theme stamped at the active plugin's last `--psp-*`
|
|
167
|
+
/// CSS CAPTURE — its first paint ([`Renderer::draw_view`]) or last
|
|
168
|
+
/// [`Renderer::restyle_all`]. `None` = no capture has happened. The
|
|
169
|
+
/// state behind [`Renderer::needs_restyle`]: restyle necessity is a
|
|
170
|
+
/// comparison against what the plugin actually captured, never
|
|
171
|
+
/// inferred from which call site mutated a theme record (the
|
|
172
|
+
/// raycasting boot double render — see
|
|
173
|
+
/// `PLUGIN_DRAW_INVARIANT_PLAN.md`, captured-theme revision). Same
|
|
174
|
+
/// lifecycle as [`Self::has_drawn`]: cleared on plugin swap,
|
|
175
|
+
/// `dispose` and `delete`; written only inside locked dispatches.
|
|
176
|
+
captured_theme: RefCell<Option<Option<String>>>,
|
|
177
|
+
|
|
178
|
+
/// The [`RenderContext`] of the currently-bound `View` (built at bind
|
|
179
|
+
/// time by the pipeline). Cleared on `dispose`/`delete`.
|
|
180
|
+
cached_context: RefCell<Option<Rc<RenderContext>>>,
|
|
181
|
+
|
|
182
|
+
/// The [`RenderContext`] pinned by the run currently holding the draw
|
|
183
|
+
/// lock, if any (invariant I5). Managed by [`ContextPin`].
|
|
184
|
+
active_context: RefCell<Option<Rc<RenderContext>>>,
|
|
185
|
+
|
|
186
|
+
/// Whether this renderer's panel is the workspace's ACTIVE panel. Pure
|
|
187
|
+
/// DATA, written synchronously by `Workspace::set_active`/`insert_panel`;
|
|
188
|
+
/// the `active` CSS class it drives is applied ONLY inside locked plugin
|
|
189
|
+
/// dispatches ([`Renderer::stamp_active`], "stamp before draw" — like the
|
|
190
|
+
/// theme), so activation-dependent chrome and the DOM it styles always
|
|
191
|
+
/// land in one paint commit.
|
|
192
|
+
is_active_panel: Cell<bool>,
|
|
193
|
+
|
|
194
|
+
/// Whether this renderer's panel is the workspace's ONLY panel. Pure DATA
|
|
195
|
+
/// like [`Self::is_active_panel`], written synchronously by the
|
|
196
|
+
/// `Workspace` panel-count mutation sites (`new`/`insert_panel`/
|
|
197
|
+
/// `remove_panel`); the `single`/`multi` CSS classes it drives are
|
|
198
|
+
/// applied ONLY inside locked plugin dispatches
|
|
199
|
+
/// ([`Renderer::stamp_active`]), for the same one-paint-commit reason.
|
|
200
|
+
is_solo_panel: Cell<bool>,
|
|
201
|
+
|
|
202
|
+
/// A `table_updated` redraw was DROPPED because this panel's plugin was
|
|
203
|
+
/// hidden (an unslotted tab-stack panel — see
|
|
204
|
+
/// `tasks::create_panel::wire_panel_render_sub`), so the plugin's
|
|
205
|
+
/// retained frame no longer reflects the bound `View`'s data. The
|
|
206
|
+
/// activation nudge consumes it to dispatch a full `plugin.update`
|
|
207
|
+
/// instead of the usual chrome-only `resize` repaint. Cleared at every
|
|
208
|
+
/// `draw_view` dispatch — set BEFORE the plugin's data fetch, so an
|
|
209
|
+
/// update landing mid-draw re-marks it and can never be lost.
|
|
210
|
+
data_stale: Cell<bool>,
|
|
97
211
|
}
|
|
98
212
|
|
|
99
213
|
/// Mutable state
|
|
@@ -104,9 +218,6 @@ pub struct RendererMutData {
|
|
|
104
218
|
plugins_idx: Option<usize>,
|
|
105
219
|
timer: MovingWindowRenderTimer,
|
|
106
220
|
selection: Option<ViewWindow>,
|
|
107
|
-
pending_plugin: Option<usize>,
|
|
108
|
-
|
|
109
|
-
/// Per-plugin config buckets, keyed by plugin name.
|
|
110
221
|
plugin_states: HashMap<String, PluginScopedConfig>,
|
|
111
222
|
}
|
|
112
223
|
|
|
@@ -138,14 +249,9 @@ impl Deref for RendererData {
|
|
|
138
249
|
}
|
|
139
250
|
}
|
|
140
251
|
|
|
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
252
|
impl Renderer {
|
|
148
253
|
pub fn new(viewer_elem: &HtmlElement) -> Self {
|
|
254
|
+
let draw_lock = DebounceMutex::default();
|
|
149
255
|
Self(Rc::new(RendererData {
|
|
150
256
|
plugin_data: RefCell::new(RendererMutData {
|
|
151
257
|
viewer_elem: viewer_elem.clone(),
|
|
@@ -154,10 +260,11 @@ impl Renderer {
|
|
|
154
260
|
plugins_idx: None,
|
|
155
261
|
selection: None,
|
|
156
262
|
timer: MovingWindowRenderTimer::default(),
|
|
157
|
-
pending_plugin: None,
|
|
158
263
|
plugin_states: HashMap::default(),
|
|
159
264
|
}),
|
|
160
|
-
|
|
265
|
+
geometry_slot: draw_lock.slot(),
|
|
266
|
+
geometry_cmd: Cell::new(None),
|
|
267
|
+
draw_lock,
|
|
161
268
|
plugin_changed: Default::default(),
|
|
162
269
|
style_changed: Default::default(),
|
|
163
270
|
reset_changed: Default::default(),
|
|
@@ -165,524 +272,176 @@ impl Renderer {
|
|
|
165
272
|
column_style_changed: Default::default(),
|
|
166
273
|
plugin_config_changed: Default::default(),
|
|
167
274
|
render_warning: Cell::new(true),
|
|
275
|
+
presize_pending: Cell::new(0),
|
|
168
276
|
on_render_limits_changed: Default::default(),
|
|
277
|
+
slot_name: Default::default(),
|
|
278
|
+
theme: Default::default(),
|
|
279
|
+
default_theme: Default::default(),
|
|
280
|
+
has_drawn: Cell::new(false),
|
|
281
|
+
captured_theme: Default::default(),
|
|
282
|
+
cached_context: Default::default(),
|
|
283
|
+
active_context: Default::default(),
|
|
284
|
+
is_active_panel: Cell::new(false),
|
|
285
|
+
is_solo_panel: Cell::new(true),
|
|
286
|
+
data_stale: Cell::new(false),
|
|
169
287
|
}))
|
|
170
288
|
}
|
|
171
289
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
&mut *self.plugin_data.borrow_mut(),
|
|
178
|
-
&mut *new_state.plugin_data.borrow_mut(),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
Ok(())
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
pub fn metadata(&self) -> Rc<PluginStaticConfig> {
|
|
185
|
-
self.borrow().metadata.clone()
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
pub fn is_chart(&self) -> bool {
|
|
189
|
-
self.metadata().name.as_str() != "Datagrid"
|
|
290
|
+
/// Set the layout slot name (panel id) under which this renderer mounts its
|
|
291
|
+
/// plugin in the viewer's light DOM, routed through the matching
|
|
292
|
+
/// `<regular-layout>` forwarding slot.
|
|
293
|
+
pub fn set_slot_name(&self, name: &str) {
|
|
294
|
+
*self.0.slot_name.borrow_mut() = Some(name.to_owned());
|
|
190
295
|
}
|
|
191
296
|
|
|
192
|
-
///
|
|
193
|
-
pub fn
|
|
194
|
-
self.
|
|
297
|
+
/// The layout slot name (panel id) for this renderer's plugin, if assigned.
|
|
298
|
+
pub fn slot_name(&self) -> Option<String> {
|
|
299
|
+
self.0.slot_name.borrow().clone()
|
|
195
300
|
}
|
|
196
301
|
|
|
197
|
-
///
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
fn active_plugin_name(&self) -> Option<String> {
|
|
201
|
-
Some(self.borrow().metadata.name.clone()).filter(|n| !n.is_empty())
|
|
302
|
+
/// Set this panel's theme name (`None` = inherit the element-level theme).
|
|
303
|
+
pub fn set_theme(&self, name: Option<String>) {
|
|
304
|
+
*self.0.theme.borrow_mut() = name;
|
|
202
305
|
}
|
|
203
306
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
pub fn all_columns_configs(&self) -> ColumnConfigMap {
|
|
208
|
-
self.active_plugin_name()
|
|
209
|
-
.and_then(|n| {
|
|
210
|
-
self.borrow()
|
|
211
|
-
.plugin_states
|
|
212
|
-
.get(&n)
|
|
213
|
-
.map(|b| b.columns.clone())
|
|
214
|
-
})
|
|
215
|
-
.unwrap_or_default()
|
|
307
|
+
/// This panel's selected theme name, if any (per-panel theming).
|
|
308
|
+
pub fn theme(&self) -> Option<String> {
|
|
309
|
+
self.0.theme.borrow().clone()
|
|
216
310
|
}
|
|
217
311
|
|
|
218
|
-
///
|
|
219
|
-
///
|
|
220
|
-
///
|
|
221
|
-
///
|
|
222
|
-
///
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
}
|
|
312
|
+
/// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] when the
|
|
313
|
+
/// effective theme is resolvable now (a named theme needs no registry; a
|
|
314
|
+
/// reset stamps only from a warm default cache — a cold one would stamp
|
|
315
|
+
/// attribute-removal). The shared "stamp-with-commit" head of every
|
|
316
|
+
/// per-panel theme mutation site.
|
|
317
|
+
pub fn set_theme_stamped(&self, theme: Option<String>) {
|
|
318
|
+
let stamp = theme.is_some() || self.default_theme().is_some();
|
|
319
|
+
self.set_theme(theme);
|
|
320
|
+
if stamp {
|
|
321
|
+
self.stamp_theme(None);
|
|
279
322
|
}
|
|
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
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
configs
|
|
327
323
|
}
|
|
328
324
|
|
|
329
|
-
///
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
.plugin_states
|
|
334
|
-
.entry(n)
|
|
335
|
-
.or_default()
|
|
336
|
-
.columns
|
|
337
|
-
.clear();
|
|
338
|
-
}
|
|
325
|
+
/// Set the cached registry default theme name (see the field docs on
|
|
326
|
+
/// [`RendererData`]).
|
|
327
|
+
pub fn set_default_theme(&self, name: Option<String>) {
|
|
328
|
+
*self.0.default_theme.borrow_mut() = name;
|
|
339
329
|
}
|
|
340
330
|
|
|
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()
|
|
331
|
+
/// The cached registry default theme name.
|
|
332
|
+
pub fn default_theme(&self) -> Option<String> {
|
|
333
|
+
self.0.default_theme.borrow().clone()
|
|
354
334
|
}
|
|
355
335
|
|
|
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
|
-
}
|
|
336
|
+
/// This panel's EFFECTIVE theme: its own ([`Self::theme`]), else the
|
|
337
|
+
/// cached registry default ([`Self::default_theme`]) — the value
|
|
338
|
+
/// [`Self::stamp_theme`] stamps.
|
|
339
|
+
pub fn effective_theme(&self) -> Option<String> {
|
|
340
|
+
self.theme().or_else(|| self.default_theme())
|
|
341
|
+
}
|
|
421
342
|
|
|
422
|
-
|
|
423
|
-
|
|
343
|
+
/// Whether the active plugin's captured `--psp-*` CSS is STALE — the
|
|
344
|
+
/// effective theme differs from the one stamped at the plugin's last
|
|
345
|
+
/// CSS capture (first paint / last restyle). The state-keyed gate for
|
|
346
|
+
/// every theme-driven `restyle_all` (the `has_drawn` analog for
|
|
347
|
+
/// `restyle` — `PLUGIN_DRAW_INVARIANT_PLAN.md`, captured-theme
|
|
348
|
+
/// revision): a caller that just mutated a theme RECORD may not need a
|
|
349
|
+
/// restyle at all if the plugin's capture already reflects the new
|
|
350
|
+
/// value (e.g. the same restore also performed the first paint,
|
|
351
|
+
/// post-stamp). `false` when no capture exists yet — the owed first
|
|
352
|
+
/// paint captures fresh by construction ("stamp before draw").
|
|
353
|
+
pub fn needs_restyle(&self) -> bool {
|
|
354
|
+
match &*self.0.captured_theme.borrow() {
|
|
355
|
+
Some(captured) => *captured != self.effective_theme(),
|
|
356
|
+
None => false,
|
|
424
357
|
}
|
|
425
358
|
}
|
|
426
359
|
|
|
427
|
-
///
|
|
428
|
-
///
|
|
429
|
-
///
|
|
430
|
-
///
|
|
360
|
+
/// Dispose a single panel's renderer: delete its active plugin and remove
|
|
361
|
+
/// this panel's element(s) from the viewer's light DOM, scoped by
|
|
362
|
+
/// `slot_name` so sibling panels are untouched (unlike
|
|
363
|
+
/// [`Self::delete`], which clears the entire light DOM). Removes the
|
|
364
|
+
/// plugin (`slot=<id>`) and any panel-scoped aux element it mounted
|
|
365
|
+
/// (e.g. the datagrid toolbar, `slot=statusbar-extra-<id>`).
|
|
431
366
|
///
|
|
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
|
-
}
|
|
367
|
+
/// The teardown is DEFERRED through this renderer's draw lock: spawned
|
|
368
|
+
/// draws are uncancellable, so a synchronous `plugin.delete()` here could
|
|
369
|
+
/// land mid-`plugin.draw()`, which the serialized-call contract forbids
|
|
370
|
+
/// (see [`crate::js::plugin`]). Deferral is invisible — the caller
|
|
371
|
+
/// (`eject_panel`) has already removed the panel from the `Workspace`, so
|
|
372
|
+
/// the same commit unmounts its frame and the plugin element sits
|
|
373
|
+
/// unslotted (unrendered) until teardown lands; panel ids are never
|
|
374
|
+
/// reused, so it can't collide with a replacement panel's slot. Draws
|
|
375
|
+
/// still queued behind the lock no-op via the synchronous session reset
|
|
376
|
+
/// (`get_view()` → `None`).
|
|
377
|
+
pub fn dispose(&self) -> ApiFuture<()> {
|
|
378
|
+
self.0.has_drawn.set(false);
|
|
379
|
+
self.0.captured_theme.borrow_mut().take();
|
|
380
|
+
self.0.cached_context.borrow_mut().take();
|
|
381
|
+
let this = self.clone();
|
|
382
|
+
ApiFuture::new(async move {
|
|
383
|
+
let renderer = this.clone();
|
|
384
|
+
this.with_lock(async move {
|
|
385
|
+
if let Some(plugin) = renderer.active_plugin() {
|
|
386
|
+
plugin.delete();
|
|
387
|
+
}
|
|
460
388
|
|
|
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
|
-
}
|
|
389
|
+
let Some(slot) = renderer.slot_name() else {
|
|
390
|
+
return Ok(());
|
|
391
|
+
};
|
|
476
392
|
|
|
477
|
-
|
|
393
|
+
let viewer = renderer.plugin_data.borrow().viewer_elem.clone();
|
|
394
|
+
let slots = [slot.clone(), format!("statusbar-extra-{slot}")];
|
|
395
|
+
let children = viewer.children();
|
|
396
|
+
let mut idx = children.length();
|
|
397
|
+
while idx > 0 {
|
|
398
|
+
idx -= 1;
|
|
399
|
+
if let Some(el) = children.item(idx)
|
|
400
|
+
&& el.get_attribute("slot").is_some_and(|s| slots.contains(&s))
|
|
401
|
+
{
|
|
402
|
+
let _ = viewer.remove_child(&el);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
478
405
|
|
|
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())
|
|
406
|
+
Ok(())
|
|
487
407
|
})
|
|
488
|
-
.
|
|
408
|
+
.await
|
|
409
|
+
})
|
|
489
410
|
}
|
|
490
411
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
.or_default()
|
|
498
|
-
.plugin
|
|
499
|
-
.clear();
|
|
412
|
+
pub fn delete(&self) -> ApiResult<()> {
|
|
413
|
+
self.0.has_drawn.set(false);
|
|
414
|
+
self.0.captured_theme.borrow_mut().take();
|
|
415
|
+
self.0.cached_context.borrow_mut().take();
|
|
416
|
+
if let Some(plugin) = self.active_plugin() {
|
|
417
|
+
plugin.delete();
|
|
500
418
|
}
|
|
501
|
-
|
|
419
|
+
self.plugin_data.borrow().viewer_elem.set_inner_text("");
|
|
420
|
+
let new_state = Self::new(&self.plugin_data.borrow().viewer_elem);
|
|
421
|
+
std::mem::swap(
|
|
422
|
+
&mut *self.plugin_data.borrow_mut(),
|
|
423
|
+
&mut *new_state.plugin_data.borrow_mut(),
|
|
424
|
+
);
|
|
502
425
|
|
|
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())
|
|
426
|
+
Ok(())
|
|
515
427
|
}
|
|
516
428
|
|
|
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())
|
|
429
|
+
pub fn metadata(&self) -> Rc<PluginStaticConfig> {
|
|
430
|
+
self.borrow().metadata.clone()
|
|
584
431
|
}
|
|
585
432
|
|
|
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
|
-
}
|
|
433
|
+
pub fn is_chart(&self) -> bool {
|
|
434
|
+
self.metadata().name.as_str() != "Datagrid"
|
|
648
435
|
}
|
|
649
436
|
|
|
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;
|
|
437
|
+
/// Whether the active plugin opts into per-column style controls.
|
|
438
|
+
pub fn can_render_column_styles(&self) -> bool {
|
|
439
|
+
self.metadata().can_render_column_styles
|
|
440
|
+
}
|
|
672
441
|
|
|
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
|
-
}
|
|
442
|
+
// ─── Per-column config (active plugin's bucket) ───────────────────
|
|
683
443
|
|
|
684
|
-
|
|
685
|
-
}
|
|
444
|
+
// ─── Plugin-level config (active plugin's bucket) ─────────────────
|
|
686
445
|
|
|
687
446
|
/// Whether the active plugin's render warning is currently armed
|
|
688
447
|
/// (i.e. an oversized view will be capped). Becomes `false` once
|
|
@@ -710,18 +469,31 @@ impl Renderer {
|
|
|
710
469
|
self.0.borrow_mut().plugin_store.plugin_configs().clone()
|
|
711
470
|
}
|
|
712
471
|
|
|
713
|
-
///
|
|
714
|
-
///
|
|
715
|
-
///
|
|
716
|
-
///
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
472
|
+
/// The currently-selected plugin, or `None` if none has been selected yet.
|
|
473
|
+
///
|
|
474
|
+
/// This is a PURE QUERY (command-query separation): unlike
|
|
475
|
+
/// [`Self::ensure_plugin_selected`] it never selects a default plugin and
|
|
476
|
+
/// never triggers the lazy `PluginStore` snapshot before a plugin exists.
|
|
477
|
+
/// Pre-draw / render-path code MUST use this and handle `None` — selecting
|
|
478
|
+
/// a plugin on the first render, before the real plugins register,
|
|
479
|
+
/// permanently pins the per-renderer store to the built-in `Debug`
|
|
480
|
+
/// (init-order race, surfaces on Safari).
|
|
481
|
+
pub fn active_plugin(&self) -> Option<JsPerspectiveViewerPlugin> {
|
|
482
|
+
// Bail on `plugins_idx` BEFORE touching `plugin_store`, so an unselected
|
|
483
|
+
// renderer never snapshots the registry.
|
|
484
|
+
let idx = self.0.borrow().plugins_idx?;
|
|
485
|
+
self.0.borrow_mut().plugin_store.plugins().get(idx).cloned()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Ensure a plugin is selected — the registry default when nothing has
|
|
489
|
+
/// ever been selected — and return it. The COMMAND half of
|
|
490
|
+
/// [`Self::active_plugin`]: this is the only path that selects a default and
|
|
491
|
+
/// snapshots the store, so it must run only when actually drawing (by which
|
|
492
|
+
/// point real plugins have registered). Errors if no plugins are
|
|
493
|
+
/// registered.
|
|
494
|
+
pub fn ensure_plugin_selected(&self) -> ApiResult<JsPerspectiveViewerPlugin> {
|
|
495
|
+
self.commit_plugin(None)?;
|
|
496
|
+
Ok(self.active_plugin().ok_or("No Plugin")?)
|
|
725
497
|
}
|
|
726
498
|
|
|
727
499
|
/// Gets a specific `JsPerspectiveViewerPlugin` by name.
|
|
@@ -735,25 +507,42 @@ impl Renderer {
|
|
|
735
507
|
Ok(result.unwrap())
|
|
736
508
|
}
|
|
737
509
|
|
|
510
|
+
/// Whether the active plugin has completed a draw. A query, not a
|
|
511
|
+
/// command — and an explicit flag rather than the old `is_connected()`
|
|
512
|
+
/// inference, which eager plugin mounting (an element in the DOM before
|
|
513
|
+
/// its first draw) would falsify.
|
|
738
514
|
pub fn is_plugin_activated(&self) -> ApiResult<bool> {
|
|
739
|
-
Ok(self
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
515
|
+
Ok(self.0.has_drawn.get())
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/// Mount the selected plugin element into the viewer's light DOM without
|
|
519
|
+
/// drawing it (see [`activate::mount_plugin`] — idempotent). Pure query:
|
|
520
|
+
/// a renderer with no selection is a no-op, deferring to the draw path's
|
|
521
|
+
/// `ensure_plugin_selected`. Used to mount eagerly — at panel creation
|
|
522
|
+
/// and at locked-draw start — so a slow first view query never leaves an
|
|
523
|
+
/// empty panel frame. NOTE eager mounting is exactly why
|
|
524
|
+
/// [`Self::is_plugin_activated`] must be an explicit has-drawn flag, not
|
|
525
|
+
/// DOM-connectedness.
|
|
526
|
+
pub fn mount_active_plugin(&self) -> ApiResult<()> {
|
|
527
|
+
if let Some(plugin) = self.active_plugin() {
|
|
528
|
+
let viewer_elem = self.0.borrow().viewer_elem.clone();
|
|
529
|
+
mount_plugin(&viewer_elem, &plugin, self.slot_name().as_deref())?;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
Ok(())
|
|
743
533
|
}
|
|
744
534
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
Ok(JsValue::UNDEFINED)
|
|
535
|
+
/// Record whether this panel is the active panel (data only — see the
|
|
536
|
+
/// field doc; the CSS class lands at the next locked plugin dispatch).
|
|
537
|
+
pub fn set_active_flag(&self, is_active: bool) {
|
|
538
|
+
self.0.is_active_panel.set(is_active);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/// Record whether this panel is the workspace's only panel (data only —
|
|
542
|
+
/// see the field doc; the CSS class lands at the next locked plugin
|
|
543
|
+
/// dispatch).
|
|
544
|
+
pub fn set_solo_flag(&self, is_solo: bool) {
|
|
545
|
+
self.0.is_solo_panel.set(is_solo);
|
|
757
546
|
}
|
|
758
547
|
|
|
759
548
|
pub fn set_throttle(&self, val: Option<f64>) {
|
|
@@ -777,10 +566,22 @@ impl Renderer {
|
|
|
777
566
|
self.0.render_warning.set(false);
|
|
778
567
|
}
|
|
779
568
|
|
|
780
|
-
|
|
569
|
+
/// Resolve a [`PluginUpdate`] against the current selection. Returns the
|
|
570
|
+
/// target plugin's index + static config when the update names a plugin
|
|
571
|
+
/// (or the registry default, for `SetDefault`) that differs from the
|
|
572
|
+
/// current selection; `None` when the update is `Missing`, names an
|
|
573
|
+
/// unknown plugin, or resolves to the already-active one.
|
|
574
|
+
///
|
|
575
|
+
/// PURE query — nothing is staged on the `Renderer`. Thread the returned
|
|
576
|
+
/// index *into* a locked draw task and commit it there with
|
|
577
|
+
/// [`Self::commit_plugin`]: plugin-swap intent must never exist outside a
|
|
578
|
+
/// running draw transaction, or an unrelated draw that wins the lock first
|
|
579
|
+
/// (e.g. a `table_updated` redraw during a `restore()`) could observe or
|
|
580
|
+
/// commit a half-applied swap.
|
|
581
|
+
pub fn resolve_plugin_update(
|
|
781
582
|
&self,
|
|
782
583
|
update: &PluginUpdate,
|
|
783
|
-
) -> Option<Rc<PluginStaticConfig
|
|
584
|
+
) -> Option<(usize, Rc<PluginStaticConfig>)> {
|
|
784
585
|
let default_plugin_name = PLUGIN_REGISTRY.default_plugin_name();
|
|
785
586
|
let name = match update {
|
|
786
587
|
PluginUpdate::Missing => return None,
|
|
@@ -795,47 +596,43 @@ impl Renderer {
|
|
|
795
596
|
);
|
|
796
597
|
|
|
797
598
|
if changed {
|
|
798
|
-
|
|
799
|
-
|
|
599
|
+
let config = self
|
|
600
|
+
.0
|
|
800
601
|
.borrow_mut()
|
|
801
602
|
.plugin_store
|
|
802
603
|
.plugin_configs()
|
|
803
604
|
.get(idx)
|
|
804
|
-
.cloned()
|
|
605
|
+
.cloned()?;
|
|
606
|
+
Some((idx, config))
|
|
805
607
|
} else {
|
|
806
608
|
None
|
|
807
609
|
}
|
|
808
610
|
}
|
|
809
611
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
612
|
+
/// Commit a plugin selection previously resolved by
|
|
613
|
+
/// [`Self::resolve_plugin_update`]. COMMAND — call only from inside a
|
|
614
|
+
/// locked draw task, so the swap lands atomically with the view rebuild
|
|
615
|
+
/// and draw it belongs to. `None` ensures *some* plugin is selected (the
|
|
616
|
+
/// registry default) without changing an existing selection. Returns
|
|
617
|
+
/// whether the active plugin changed (`None`'s default-selection case
|
|
618
|
+
/// reports `false`, preserving first-selection semantics for the
|
|
619
|
+
/// swap-restore pass in the draw tasks).
|
|
620
|
+
pub fn commit_plugin(&self, idx: Option<usize>) -> ApiResult<bool> {
|
|
621
|
+
let idx = match idx {
|
|
622
|
+
Some(idx) => idx,
|
|
623
|
+
None => {
|
|
624
|
+
if self.0.borrow().plugins_idx.is_none() {
|
|
625
|
+
let name = PLUGIN_REGISTRY.default_plugin_name();
|
|
626
|
+
let idx = self
|
|
627
|
+
.find_plugin_idx(&name)
|
|
628
|
+
.ok_or_else(|| JsValue::from(format!("Unknown plugin '{name}'")))?;
|
|
629
|
+
|
|
630
|
+
self.commit_plugin_idx(idx)?;
|
|
631
|
+
}
|
|
831
632
|
|
|
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}'")))?;
|
|
633
|
+
return Ok(false);
|
|
634
|
+
},
|
|
635
|
+
};
|
|
839
636
|
|
|
840
637
|
let changed = !matches!(
|
|
841
638
|
self.0.borrow().plugins_idx,
|
|
@@ -849,11 +646,16 @@ impl Renderer {
|
|
|
849
646
|
Ok(changed)
|
|
850
647
|
}
|
|
851
648
|
|
|
852
|
-
/// Shared tail of `
|
|
649
|
+
/// Shared tail of [`Self::commit_plugin`]: switch the
|
|
853
650
|
/// active plugin to `idx`, swap in its cached `PluginStaticConfig`,
|
|
854
651
|
/// reset the per-plugin render-warning flag, and fire
|
|
855
652
|
/// `plugin_changed`.
|
|
856
653
|
fn commit_plugin_idx(&self, idx: usize) -> ApiResult<()> {
|
|
654
|
+
// The newly-selected plugin element has not drawn (a swap keeps the
|
|
655
|
+
// OLD plugin mounted until the new one's draw lands) — and has
|
|
656
|
+
// captured no CSS.
|
|
657
|
+
self.0.has_drawn.set(false);
|
|
658
|
+
self.0.captured_theme.borrow_mut().take();
|
|
857
659
|
self.borrow_mut().plugins_idx = Some(idx);
|
|
858
660
|
let config = self
|
|
859
661
|
.0
|
|
@@ -866,7 +668,10 @@ impl Renderer {
|
|
|
866
668
|
|
|
867
669
|
self.borrow_mut().metadata = config.clone();
|
|
868
670
|
self.0.render_warning.set(true);
|
|
869
|
-
|
|
671
|
+
// `commit_plugin_idx` is called *by* the selection path, so it must use
|
|
672
|
+
// the pure query (never `ensure_plugin_selected`, which would recurse
|
|
673
|
+
// through `commit_plugin`). `plugins_idx` was just set above.
|
|
674
|
+
let plugin: JsPerspectiveViewerPlugin = self.active_plugin().ok_or("No Plugin")?;
|
|
870
675
|
|
|
871
676
|
// Push the newly-activated plugin's stored bucket through
|
|
872
677
|
// `plugin.restore` so the swap immediately reflects any
|
|
@@ -886,192 +691,6 @@ impl Renderer {
|
|
|
886
691
|
Ok(())
|
|
887
692
|
}
|
|
888
693
|
|
|
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
694
|
pub fn render_timer(&self) -> MovingWindowRenderTimer {
|
|
1076
695
|
self.0.borrow().timer.clone()
|
|
1077
696
|
}
|
|
@@ -1080,10 +699,6 @@ impl Renderer {
|
|
|
1080
699
|
let short_name = make_short_name(name);
|
|
1081
700
|
let mut borrowed = self.0.borrow_mut();
|
|
1082
701
|
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
702
|
let short_names: Vec<String> = configs.iter().map(|c| make_short_name(&c.name)).collect();
|
|
1088
703
|
if let Some(i) = short_names.iter().position(|n| n == &short_name) {
|
|
1089
704
|
return Some(i);
|
|
@@ -1107,14 +722,7 @@ impl Renderer {
|
|
|
1107
722
|
/// suitable for passing as a Yew prop. Called by the root component
|
|
1108
723
|
/// whenever a renderer-related PubSub event fires.
|
|
1109
724
|
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();
|
|
725
|
+
let has_plugin = self.active_plugin().is_some();
|
|
1118
726
|
if has_plugin {
|
|
1119
727
|
let config = self.metadata();
|
|
1120
728
|
let plugin_name = Some(config.name.clone());
|
|
@@ -1125,8 +733,8 @@ impl Renderer {
|
|
|
1125
733
|
.map(|c| c.name.clone())
|
|
1126
734
|
.collect::<Vec<_>>()
|
|
1127
735
|
.into();
|
|
1128
|
-
let plugin_config = PtrEqRc::new(self.get_plugin_config());
|
|
1129
736
|
|
|
737
|
+
let plugin_config = PtrEqRc::new(self.get_plugin_config());
|
|
1130
738
|
RendererProps {
|
|
1131
739
|
plugin_name,
|
|
1132
740
|
config,
|
|
@@ -1147,65 +755,3 @@ impl Renderer {
|
|
|
1147
755
|
}
|
|
1148
756
|
}
|
|
1149
757
|
}
|
|
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
|
-
}
|