@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.
Files changed (227) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/extensions.d.ts +22 -3
  24. package/dist/esm/perspective-viewer.d.ts +9 -0
  25. package/dist/esm/perspective-viewer.inline.js +20 -1
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +20 -1
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/plugin.d.ts +71 -5
  30. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  32. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  33. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  34. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  35. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  36. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  37. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  38. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -0
  39. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  40. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  41. package/dist/esm/ts-rs/ViewerConfig.d.ts +8 -2
  42. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
  43. package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
  44. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  45. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  46. package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
  47. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  50. package/dist/wasm/perspective-viewer.d.ts +299 -78
  51. package/dist/wasm/perspective-viewer.js +544 -186
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  54. package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
  55. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
  56. package/package.json +3 -2
  57. package/src/css/_context_menu.css +14 -0
  58. package/src/css/column-selector.css +1 -0
  59. package/src/css/column-settings-panel.css +68 -3
  60. package/src/css/column-style.css +4 -3
  61. package/src/css/config-selector.css +22 -1
  62. package/src/css/containers/context-menu.css +107 -0
  63. package/src/css/containers/tabs.css +2 -2
  64. package/src/css/panel-tab.css +193 -0
  65. package/src/css/plugin-selector.css +7 -10
  66. package/src/css/status-bar.css +75 -81
  67. package/src/css/viewer.css +188 -70
  68. package/src/rust/components/column_dropdown.rs +40 -30
  69. package/src/rust/components/column_selector/active_column.rs +28 -45
  70. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  71. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  72. package/src/rust/components/column_selector/config_selector.rs +40 -82
  73. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  74. package/src/rust/components/column_selector/filter_column.rs +5 -15
  75. package/src/rust/components/column_selector/inactive_column.rs +11 -9
  76. package/src/rust/components/column_selector/sort_column.rs +3 -6
  77. package/src/rust/components/column_selector.rs +25 -14
  78. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  79. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  80. package/src/rust/components/column_settings_sidebar.rs +163 -16
  81. package/src/rust/components/containers/dragdrop_list.rs +73 -3
  82. package/src/rust/components/containers/sidebar.rs +99 -22
  83. package/src/rust/components/containers/split_panel.rs +181 -62
  84. package/src/rust/components/context_menu.rs +130 -0
  85. package/src/rust/components/editable_header.rs +2 -0
  86. package/src/rust/components/form/debug.rs +3 -0
  87. package/src/rust/components/global_filter_bar.rs +76 -0
  88. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  89. package/src/rust/components/main_panel/msg.rs +62 -0
  90. package/src/rust/components/main_panel/presize.rs +220 -0
  91. package/src/rust/components/main_panel/reconcile.rs +224 -0
  92. package/src/rust/components/main_panel/render.rs +385 -0
  93. package/src/rust/components/main_panel/update.rs +184 -0
  94. package/src/rust/components/main_panel.rs +316 -71
  95. package/src/rust/components/mod.rs +5 -0
  96. package/src/rust/components/panel_menu.rs +493 -0
  97. package/src/rust/components/panel_tab.rs +510 -0
  98. package/src/rust/components/plugin_tab.rs +1 -1
  99. package/src/rust/components/portal.rs +7 -0
  100. package/src/rust/components/settings_panel.rs +13 -5
  101. package/src/rust/components/status_bar.rs +110 -178
  102. package/src/rust/components/status_indicator.rs +6 -9
  103. package/src/rust/components/style/mod.rs +5 -0
  104. package/src/rust/components/style/style_provider.rs +2 -0
  105. package/src/rust/components/viewer/filters.rs +133 -0
  106. package/src/rust/components/viewer/msg.rs +182 -0
  107. package/src/rust/components/viewer/panels.rs +326 -0
  108. package/src/rust/components/viewer/render.rs +255 -0
  109. package/src/rust/components/viewer/settings.rs +434 -0
  110. package/src/rust/components/viewer/snapshots.rs +134 -0
  111. package/src/rust/components/viewer/wiring.rs +505 -0
  112. package/src/rust/components/viewer.rs +245 -756
  113. package/src/rust/components/window_editor.rs +1013 -0
  114. package/src/rust/config/export_method.rs +1 -1
  115. package/src/rust/config/mod.rs +4 -0
  116. package/src/rust/config/options.rs +71 -0
  117. package/src/rust/config/viewer_config.rs +24 -2
  118. package/src/rust/config/workspace_config.rs +92 -0
  119. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  120. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  121. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  122. package/src/rust/custom_elements/viewer.rs +1015 -286
  123. package/src/rust/custom_events.rs +212 -61
  124. package/src/rust/js/mod.rs +2 -0
  125. package/src/rust/js/plugin.rs +297 -10
  126. package/src/rust/js/regular_layout.rs +258 -0
  127. package/src/rust/lib.rs +24 -2
  128. package/src/rust/presentation/column_locator.rs +16 -1
  129. package/src/rust/presentation.rs +66 -33
  130. package/src/rust/queries/columns_iter_set.rs +7 -1
  131. package/src/rust/queries/get_viewer_config.rs +29 -11
  132. package/src/rust/queries/plugin_column_styles.rs +2 -2
  133. package/src/rust/renderer/activate.rs +41 -14
  134. package/src/rust/renderer/dispatch.rs +463 -0
  135. package/src/rust/renderer/plugin_config.rs +495 -0
  136. package/src/rust/renderer/registry.rs +18 -6
  137. package/src/rust/renderer/render_timer.rs +29 -10
  138. package/src/rust/renderer.rs +384 -838
  139. package/src/rust/session/column_defaults_update.rs +26 -8
  140. package/src/rust/session/drag_drop_update.rs +18 -0
  141. package/src/rust/session/metadata.rs +66 -9
  142. package/src/rust/session/replace_expression_update.rs +1 -0
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +755 -264
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/eject.rs +64 -23
  153. package/src/rust/tasks/mod.rs +30 -4
  154. package/src/rust/tasks/pipeline.rs +411 -0
  155. package/src/rust/tasks/presize_panels.rs +324 -0
  156. package/src/rust/tasks/reset_all.rs +22 -12
  157. package/src/rust/tasks/resize_observer.rs +124 -87
  158. package/src/rust/tasks/restore_and_render.rs +80 -86
  159. package/src/rust/tasks/restore_panel.rs +145 -0
  160. package/src/rust/tasks/send_column_config.rs +5 -2
  161. package/src/rust/tasks/send_plugin_config.rs +5 -2
  162. package/src/rust/tasks/set_edit_mode.rs +59 -0
  163. package/src/rust/tasks/sync_update_panels.rs +152 -0
  164. package/src/rust/tasks/table_lifecycle.rs +158 -0
  165. package/src/rust/tasks/update_theme.rs +56 -12
  166. package/src/rust/utils/browser/dragdrop.rs +12 -0
  167. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  168. package/src/rust/utils/completion.rs +56 -0
  169. package/src/rust/utils/custom_element.rs +5 -0
  170. package/src/rust/utils/debounce.rs +189 -17
  171. package/src/rust/utils/mod.rs +6 -0
  172. package/src/rust/utils/pending_effects.rs +62 -0
  173. package/src/rust/utils/spawn.rs +33 -0
  174. package/src/rust/utils/weak_scope.rs +0 -10
  175. package/src/rust/workspace.rs +846 -0
  176. package/src/svg/active-indicator.svg +3 -0
  177. package/src/svg/pin-icon.svg +5 -0
  178. package/src/themes/botanical.css +5 -2
  179. package/src/themes/defaults.css +54 -2
  180. package/src/themes/dracula.css +6 -3
  181. package/src/themes/gruvbox-dark.css +5 -2
  182. package/src/themes/gruvbox.css +5 -2
  183. package/src/themes/icons.css +5 -2
  184. package/src/themes/intl/de.css +7 -0
  185. package/src/themes/intl/es.css +7 -0
  186. package/src/themes/intl/fr.css +7 -0
  187. package/src/themes/intl/ja.css +7 -0
  188. package/src/themes/intl/pt.css +7 -0
  189. package/src/themes/intl/zh.css +7 -0
  190. package/src/themes/intl.css +10 -1
  191. package/src/themes/monokai.css +5 -2
  192. package/src/themes/phosphor.css +5 -2
  193. package/src/themes/pro-dark.css +5 -2
  194. package/src/themes/pro.css +2 -1
  195. package/src/themes/solarized-dark.css +5 -2
  196. package/src/themes/solarized.css +5 -2
  197. package/src/themes/vaporwave.css +5 -2
  198. package/src/ts/bootstrap.ts +8 -7
  199. package/src/ts/extensions.ts +30 -2
  200. package/src/ts/perspective-viewer.ts +10 -0
  201. package/src/ts/plugin.ts +72 -5
  202. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  203. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  204. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  205. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  206. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  207. package/src/ts/ts-rs/Layout.ts +9 -0
  208. package/src/ts/ts-rs/Orientation.ts +6 -0
  209. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  210. package/src/ts/ts-rs/PanelViewerConfig.ts +16 -0
  211. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  212. package/src/ts/ts-rs/TabLayout.ts +11 -0
  213. package/src/ts/ts-rs/ViewerConfig.ts +7 -2
  214. package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
  215. package/src/ts/ts-rs/WindowAggregate.ts +3 -0
  216. package/src/ts/ts-rs/WindowSort.ts +9 -0
  217. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  218. package/src/ts/ts-rs/WindowSpec.ts +46 -0
  219. package/src/ts/ts-rs/Windows.ts +10 -0
  220. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  221. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  222. package/src/rust/tasks/intersection_observer.rs +0 -98
  223. package/src/rust/tasks/update_and_render.rs +0 -110
  224. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
  225. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
  226. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
  227. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
@@ -0,0 +1,495 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! The [`Renderer`]'s per-plugin config state: the plugin-level and
14
+ //! per-column buckets (`plugin_states`), their schema-aware strip/merge
15
+ //! write paths, and the restore-prep materialized snapshot.
16
+
17
+ use std::collections::HashMap;
18
+
19
+ use futures::future::join_all;
20
+ use perspective_client::config::ViewConfig;
21
+ use perspective_js::utils::{ApiResult, JsValueSerdeExt};
22
+ use serde_json::Value;
23
+ use wasm_bindgen::prelude::*;
24
+
25
+ use super::Renderer;
26
+ use crate::config::*;
27
+ use crate::queries::resolve_abs_max;
28
+ use crate::session::Session;
29
+
30
+ /// A per-column config map. Each inner [`serde_json::Map`] is a flat collection
31
+ /// of plugin-defined JSON keys whose shape is dictated by the active plugin's
32
+ /// [`crate::config::ColumnConfigSchema`].
33
+ pub type ColumnConfigMap = HashMap<String, serde_json::Map<String, serde_json::Value>>;
34
+
35
+ /// Per-plugin config bucket. Holds the per-column style map and the
36
+ /// plugin-level config map for one plugin. Buckets are keyed by plugin
37
+ /// name in [`RendererMutData::plugin_states`], so foreign keys from a
38
+ /// different plugin physically cannot appear in the active plugin's
39
+ /// bucket.
40
+ #[derive(Clone, Debug, Default)]
41
+ pub struct PluginScopedConfig {
42
+ pub columns: ColumnConfigMap,
43
+ pub plugin: serde_json::Map<String, serde_json::Value>,
44
+ }
45
+
46
+ impl Renderer {
47
+ /// Name of the currently-active plugin (used as the key into
48
+ /// `plugin_states`). Returns `None` when no plugin has been
49
+ /// activated yet.
50
+ fn active_plugin_name(&self) -> Option<String> {
51
+ Some(self.borrow().metadata.name.clone()).filter(|n| !n.is_empty())
52
+ }
53
+
54
+ /// Snapshot of the active plugin's per-column config map.
55
+ pub fn all_columns_configs(&self) -> ColumnConfigMap {
56
+ self.active_plugin_name()
57
+ .and_then(|n| {
58
+ self.borrow()
59
+ .plugin_states
60
+ .get(&n)
61
+ .map(|b| b.columns.clone())
62
+ })
63
+ .unwrap_or_default()
64
+ }
65
+
66
+ /// Restore-prep snapshot: like [`Self::all_columns_configs`], but
67
+ /// for each column also materializes any `ControlSpec::Number`
68
+ /// fields the schema declares with `include: true` that aren't
69
+ /// already in the bucket entry. The materialized value is the
70
+ /// schema's `default`, which the schema computes from cached
71
+ /// column stats (via [`Self::query_column_config_schema`]).
72
+ pub async fn all_columns_configs_materialized(
73
+ &self,
74
+ view_config: &ViewConfig,
75
+ session: &Session,
76
+ ) -> ColumnConfigMap {
77
+ let mut configs = self.all_columns_configs();
78
+ let mut to_warm: Vec<String> = vec![];
79
+ for (col, entry) in &configs {
80
+ if session
81
+ .get_column_stats(col)
82
+ .and_then(|s| s.abs_max)
83
+ .is_some()
84
+ {
85
+ continue;
86
+ }
87
+ let Ok(schema) =
88
+ self.query_column_config_schema(view_config, session, col, Some(entry))
89
+ else {
90
+ continue;
91
+ };
92
+ let needs_warm = schema.fields.iter().any(|f| {
93
+ matches!(
94
+ f,
95
+ ControlSpec::Number {
96
+ key,
97
+ include: Some(true),
98
+ ..
99
+ } if !entry.contains_key(key)
100
+ )
101
+ });
102
+ if needs_warm {
103
+ to_warm.push(col.clone());
104
+ }
105
+ }
106
+
107
+ if !to_warm.is_empty() {
108
+ let metadata = session.metadata().clone();
109
+ let view = session.get_view();
110
+ let futs = to_warm
111
+ .iter()
112
+ .map(|c| resolve_abs_max(session, &metadata, view.as_ref(), c.as_str()));
113
+ join_all(futs).await;
114
+ }
115
+
116
+ for (col, entry) in &mut configs {
117
+ let Ok(schema) =
118
+ self.query_column_config_schema(view_config, session, col, Some(entry))
119
+ else {
120
+ continue;
121
+ };
122
+
123
+ for field in &schema.fields {
124
+ let ControlSpec::Number {
125
+ key,
126
+ default,
127
+ include: Some(true),
128
+ ..
129
+ } = field
130
+ else {
131
+ continue;
132
+ };
133
+
134
+ if entry.contains_key(key) {
135
+ continue;
136
+ }
137
+
138
+ let Some(num) = serde_json::Number::from_f64(*default) else {
139
+ continue;
140
+ };
141
+
142
+ entry.insert(key.clone(), serde_json::Value::Number(num));
143
+ }
144
+ }
145
+
146
+ configs
147
+ }
148
+
149
+ /// Clear the active plugin's per-column config map.
150
+ pub fn reset_columns_configs(&self) {
151
+ if let Some(n) = self.active_plugin_name() {
152
+ self.borrow_mut()
153
+ .plugin_states
154
+ .entry(n)
155
+ .or_default()
156
+ .columns
157
+ .clear();
158
+ }
159
+ }
160
+
161
+ /// Clone of the active plugin's per-column entry for `column_name`,
162
+ /// or `None` if no value is stored.
163
+ pub fn get_columns_config(
164
+ &self,
165
+ column_name: &str,
166
+ ) -> Option<serde_json::Map<String, serde_json::Value>> {
167
+ let n = self.active_plugin_name()?;
168
+ self.borrow()
169
+ .plugin_states
170
+ .get(&n)?
171
+ .columns
172
+ .get(column_name)
173
+ .cloned()
174
+ }
175
+
176
+ /// Wholesale update the active plugin's per-column config map
177
+ pub fn update_columns_configs(
178
+ &self,
179
+ view_config: &ViewConfig,
180
+ session: &Session,
181
+ update: ColumnConfigUpdate,
182
+ ) -> bool {
183
+ let Some(n) = self.active_plugin_name() else {
184
+ return false;
185
+ };
186
+
187
+ match update {
188
+ OptionalUpdate::SetDefault => {
189
+ let mut st = self.borrow_mut();
190
+ let bucket = st.plugin_states.entry(n).or_default();
191
+ let was_nonempty = !bucket.columns.is_empty();
192
+ bucket.columns.clear();
193
+ was_nonempty
194
+ },
195
+ OptionalUpdate::Missing => false,
196
+ OptionalUpdate::Update(map) => {
197
+ let stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> = map
198
+ .into_iter()
199
+ .map(|(col, mut cfg)| {
200
+ if let Ok(schema) =
201
+ self.query_column_config_schema(view_config, session, &col, Some(&cfg))
202
+ {
203
+ let active = schema.active_keys();
204
+ cfg.retain(|k, _| active.contains(k));
205
+ strip_default_values(&schema, &mut cfg);
206
+ }
207
+
208
+ (col, cfg)
209
+ })
210
+ .collect();
211
+
212
+ let mut st = self.borrow_mut();
213
+ let bucket = st.plugin_states.entry(n).or_default();
214
+ let mut changed = false;
215
+ for (col, cfg) in stripped {
216
+ if cfg.is_empty() {
217
+ if bucket.columns.remove(&col).is_some() {
218
+ changed = true;
219
+ }
220
+ } else {
221
+ match bucket.columns.insert(col, cfg.clone()) {
222
+ None => changed = true,
223
+ Some(old) if old != cfg => changed = true,
224
+ _ => {},
225
+ }
226
+ }
227
+ }
228
+
229
+ changed
230
+ },
231
+ }
232
+ }
233
+
234
+ /// Apply a single schema-field update from the column-style UI to
235
+ /// the active plugin's bucket. Clears the keys the field owns,
236
+ /// then splices in the partial new sub-state. Drops empty
237
+ /// entries.
238
+ pub fn update_columns_config_field(
239
+ &self,
240
+ view_config: &ViewConfig,
241
+ session: &Session,
242
+ column_name: String,
243
+ mut update: ColumnConfigFieldUpdate,
244
+ ) {
245
+ let Some(n) = self.active_plugin_name() else {
246
+ return;
247
+ };
248
+
249
+ let current_value = self.get_columns_config(&column_name);
250
+ if let Ok(schema) = self.query_column_config_schema(
251
+ view_config,
252
+ session,
253
+ &column_name,
254
+ current_value.as_ref(),
255
+ ) {
256
+ strip_default_values(&schema, &mut update.value);
257
+ }
258
+
259
+ let mut st = self.borrow_mut();
260
+ let bucket = st.plugin_states.entry(n).or_default();
261
+ let entry = bucket.columns.entry(column_name.clone()).or_default();
262
+ for k in &update.keys {
263
+ entry.remove(k);
264
+ }
265
+ for (k, v) in update.value {
266
+ if update.keys.contains(&k) {
267
+ entry.insert(k, v);
268
+ }
269
+ }
270
+ if entry.is_empty() {
271
+ bucket.columns.remove(&column_name);
272
+ }
273
+ }
274
+
275
+ /// Snapshot of the active plugin's plugin-level config map.
276
+ pub fn get_plugin_config(&self) -> serde_json::Map<String, serde_json::Value> {
277
+ self.active_plugin_name()
278
+ .and_then(|n| {
279
+ self.borrow()
280
+ .plugin_states
281
+ .get(&n)
282
+ .map(|b| b.plugin.clone())
283
+ })
284
+ .unwrap_or_default()
285
+ }
286
+
287
+ /// Clear the active plugin's plugin-level config map.
288
+ pub fn reset_plugin_config(&self) {
289
+ if let Some(n) = self.active_plugin_name() {
290
+ self.borrow_mut()
291
+ .plugin_states
292
+ .entry(n)
293
+ .or_default()
294
+ .plugin
295
+ .clear();
296
+ }
297
+ }
298
+
299
+ /// Synchronously query the active plugin's
300
+ /// [`ColumnConfigSchema`] used to gate plugin-config strip logic.
301
+ fn query_plugin_config_schema(
302
+ &self,
303
+ view_config: &ViewConfig,
304
+ ) -> ApiResult<ColumnConfigSchema> {
305
+ let plugin = self.ensure_plugin_selected()?;
306
+ let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
307
+ let raw = plugin._plugin_config_schema(&view_config_js)?;
308
+ serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
309
+ }
310
+
311
+ /// Per-column counterpart of [`query_plugin_config_schema`]. Used by
312
+ /// the columns-config write paths (strip-on-write) and the
313
+ /// restore-prep snapshot (materialize-on-read).
314
+ fn query_column_config_schema(
315
+ &self,
316
+ view_config: &ViewConfig,
317
+ session: &Session,
318
+ column_name: &str,
319
+ current_value: Option<&serde_json::Map<String, serde_json::Value>>,
320
+ ) -> ApiResult<ColumnConfigSchema> {
321
+ let plugin = self.ensure_plugin_selected()?;
322
+ let plugin_config = self.metadata();
323
+ let names = &plugin_config.config_column_names;
324
+ let group = view_config
325
+ .columns
326
+ .iter()
327
+ .position(|maybe_s| maybe_s.as_deref() == Some(column_name))
328
+ .and_then(|idx| names.get(idx))
329
+ .map(|s| s.as_str());
330
+
331
+ if !session.metadata().has_view_schema() {
332
+ return Err(JsValue::from("view_schema not initialized").into());
333
+ }
334
+ let Some(view_type) = session.metadata().get_column_view_type(column_name) else {
335
+ return Ok(ColumnConfigSchema { fields: vec![] });
336
+ };
337
+
338
+ let current_js = JsValue::from_serde_ext(&current_value).unwrap_or(JsValue::NULL);
339
+ let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
340
+ let stats = session.get_column_stats(column_name).unwrap_or_default();
341
+ let stats_json = serde_json::json!({
342
+ "abs_max": stats.abs_max,
343
+ });
344
+ let stats_js = JsValue::from_serde_ext(&stats_json).unwrap_or(JsValue::NULL);
345
+
346
+ let raw = plugin._column_config_schema(
347
+ &view_type.to_string(),
348
+ group,
349
+ column_name,
350
+ &current_js,
351
+ &view_config_js,
352
+ &stats_js,
353
+ )?;
354
+
355
+ serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
356
+ }
357
+
358
+ /// Wholesale update the active plugin's plugin-level config map.
359
+ /// Entries whose value equals the schema-declared default are
360
+ /// treated as "reset this key" — the corresponding bucket entry
361
+ /// is cleared rather than the default being stored literally.
362
+ /// Keys absent from the incoming map are left alone (merge
363
+ /// semantics for the non-default subset).
364
+ pub fn update_plugin_config(
365
+ &self,
366
+ view_config: &ViewConfig,
367
+ update: PluginConfigUpdate,
368
+ ) -> bool {
369
+ let Some(n) = self.active_plugin_name() else {
370
+ return false;
371
+ };
372
+
373
+ let schema = self.query_plugin_config_schema(view_config).ok();
374
+ let mut st = self.borrow_mut();
375
+ let bucket = st.plugin_states.entry(n).or_default();
376
+ match update {
377
+ OptionalUpdate::SetDefault => {
378
+ let changed = !bucket.plugin.is_empty();
379
+ bucket.plugin.clear();
380
+ changed
381
+ },
382
+ OptionalUpdate::Missing => false,
383
+ OptionalUpdate::Update(mut map) => {
384
+ let mut changed = false;
385
+ if let Some(s) = &schema {
386
+ let active = s.active_keys();
387
+ map.retain(|k, _| active.contains(k));
388
+ map.retain(|key, value| {
389
+ let is_default = s
390
+ .fields
391
+ .iter()
392
+ .any(|spec| matches_declared_default(spec, key, value));
393
+ if is_default {
394
+ if bucket.plugin.remove(key).is_some() {
395
+ changed = true;
396
+ }
397
+ false
398
+ } else {
399
+ true
400
+ }
401
+ });
402
+ }
403
+
404
+ for (k, v) in map {
405
+ let prev = bucket.plugin.insert(k, v.clone());
406
+ if prev.as_ref() != Some(&v) {
407
+ changed = true;
408
+ }
409
+ }
410
+
411
+ changed
412
+ },
413
+ }
414
+ }
415
+
416
+ /// Apply a single schema-field update from the plugin-settings UI
417
+ pub fn update_plugin_config_field(
418
+ &self,
419
+ view_config: &ViewConfig,
420
+ mut update: ColumnConfigFieldUpdate,
421
+ ) -> bool {
422
+ let Some(n) = self.active_plugin_name() else {
423
+ return false;
424
+ };
425
+
426
+ if let Ok(schema) = self.query_plugin_config_schema(view_config) {
427
+ strip_default_values(&schema, &mut update.value);
428
+ }
429
+
430
+ let mut st = self.borrow_mut();
431
+ let bucket = st.plugin_states.entry(n).or_default();
432
+ let mut changed = false;
433
+
434
+ for k in &update.keys {
435
+ if let Some(v) = update.value.get(k) {
436
+ let prev = bucket.plugin.insert(k.to_string(), v.clone());
437
+ if prev.as_ref() != Some(v) {
438
+ changed = true;
439
+ }
440
+ } else if bucket.plugin.remove(k).is_some() {
441
+ changed = true;
442
+ }
443
+ }
444
+
445
+ changed
446
+ }
447
+ }
448
+
449
+ fn strip_default_values(
450
+ schema: &ColumnConfigSchema,
451
+ map: &mut serde_json::Map<String, serde_json::Value>,
452
+ ) {
453
+ map.retain(|key, value| {
454
+ !schema
455
+ .fields
456
+ .iter()
457
+ .any(|spec| matches_declared_default(spec, key, value))
458
+ });
459
+ }
460
+
461
+ fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> bool {
462
+ match spec {
463
+ ControlSpec::Enum {
464
+ key: k, default, ..
465
+ } if k == key => value.as_str() == Some(default.as_str()),
466
+ ControlSpec::Bool {
467
+ key: k, default, ..
468
+ } if k == key => value.as_bool() == Some(*default),
469
+ ControlSpec::Number {
470
+ key: k,
471
+ include: Some(true),
472
+ ..
473
+ } if k == key => false,
474
+ ControlSpec::Number {
475
+ key: k, default, ..
476
+ } if k == key => value.as_f64() == Some(*default),
477
+ ControlSpec::String {
478
+ key: k, default, ..
479
+ } if k == key => value.as_str() == Some(default.as_str()),
480
+ ControlSpec::Color {
481
+ key: k, default, ..
482
+ } if k == key => value.as_str() == Some(default.as_str()),
483
+ ControlSpec::ColorRange {
484
+ key_pos,
485
+ default_pos,
486
+ ..
487
+ } if key_pos == key => value.as_str() == Some(default_pos.as_str()),
488
+ ControlSpec::ColorRange {
489
+ key_neg,
490
+ default_neg,
491
+ ..
492
+ } if key_neg == key => value.as_str() == Some(default_neg.as_str()),
493
+ _ => false,
494
+ }
495
+ }
@@ -43,12 +43,12 @@ pub impl LocalKey<Rc<RefCell<Vec<PluginRecord>>>> {
43
43
  let mut elements = vec![];
44
44
  for plugin in plugins.borrow().iter() {
45
45
  let element = create_plugin(&plugin.tag_name);
46
- let style = element.unchecked_ref::<HtmlElement>().style();
47
- style.set_property("position", "absolute")?;
48
- style.set_property("top", "0")?;
49
- style.set_property("right", "0")?;
50
- style.set_property("bottom", "0")?;
51
- style.set_property("left", "0")?;
46
+ // let style = element.unchecked_ref::<HtmlElement>().style();
47
+ // style.set_property("position", "absolute")?;
48
+ // style.set_property("top", "0")?;
49
+ // style.set_property("right", "0")?;
50
+ // style.set_property("bottom", "0")?;
51
+ // style.set_property("left", "0")?;
52
52
  elements.push(element);
53
53
  }
54
54
 
@@ -114,6 +114,18 @@ pub impl LocalKey<Rc<RefCell<Vec<PluginRecord>>>> {
114
114
  self.with(|plugin| {
115
115
  let plugin_inst = create_plugin(tag_name);
116
116
  let config = Rc::new(plugin_inst.read_static_config());
117
+
118
+ // Warm the per-tag capability memo on the probe instance, so
119
+ // the first render never pays the `Reflect` sweep — and surface
120
+ // a missing REQUIRED `draw` at registration instead of as a
121
+ // `TypeError` at first paint.
122
+ if !plugin_inst.capabilities().draw {
123
+ tracing::warn!(
124
+ "Plugin '{}' does not implement the required `draw` method",
125
+ tag_name
126
+ );
127
+ }
128
+
117
129
  let record = PluginRecord {
118
130
  tag_name: tag_name.to_owned(),
119
131
  config,
@@ -33,6 +33,15 @@ pub struct RenderTimerState {
33
33
  render_times: VecDeque<f64>,
34
34
  total_render_count: u32,
35
35
  start_time: f64,
36
+
37
+ /// Bumped by every `visibilitychange` reset. A [`MovingWindowRenderTimer::
38
+ /// capture_time`] whose awaited render spans a visibility flip measured
39
+ /// the hidden gap, not the render — it compares its starting epoch
40
+ /// against this and discards the sample, closing the hole the reset
41
+ /// alone leaves: an in-flight draw resolves AFTER the show-time reset,
42
+ /// so its giant sample would land in the freshly-cleared window and pin
43
+ /// the throttle at its cap.
44
+ epoch: u64,
36
45
  }
37
46
 
38
47
  /// Serialization of snapshot stats for the JS API call.
@@ -48,22 +57,24 @@ pub struct RenderTimerStats {
48
57
  impl MovingWindowRenderTimer {
49
58
  pub async fn capture_time<T>(&self, f: impl Future<Output = T>) -> T {
50
59
  let perf = global::window().performance().unwrap();
51
- let start = match *self.0.borrow() {
52
- RenderTimerType::Constant(_) => 0_f64,
53
- RenderTimerType::Moving(..) => perf.now(),
60
+ let (start, epoch) = match &*self.0.borrow() {
61
+ RenderTimerType::Constant(_) => (0_f64, 0_u64),
62
+ RenderTimerType::Moving(_, timings) => (perf.now(), timings.borrow().epoch),
54
63
  };
55
64
 
56
65
  let result = f.await;
57
66
  match &mut *self.0.borrow_mut() {
58
67
  RenderTimerType::Moving(_, timings) => {
59
68
  let mut stats = timings.borrow_mut();
60
- let now = perf.now();
61
- stats.render_times.push_back(now - start);
62
- if stats.render_times.len() > 5 {
63
- stats.render_times.pop_front();
69
+ if stats.epoch == epoch {
70
+ let now = perf.now();
71
+ stats.render_times.push_back(now - start);
72
+ if stats.render_times.len() > 5 {
73
+ stats.render_times.pop_front();
74
+ }
75
+
76
+ stats.total_render_count += 1;
64
77
  }
65
-
66
- stats.total_render_count += 1;
67
78
  },
68
79
  RenderTimerType::Constant(_) => (),
69
80
  };
@@ -143,10 +154,17 @@ impl RenderTimerState {
143
154
  impl RefCell<RenderTimerState> {
144
155
  /// We need to clear the throttle queue when the browser tab is hidden, else
145
156
  /// the next frame timing will be the time the tab was hidden + render time.
157
+ /// The epoch bump (not just the clear) is what invalidates an IN-FLIGHT
158
+ /// `capture_time` — its sample resolves after this reset runs.
146
159
  fn register_on_visibility_change(self: &Rc<Self>) -> Closure<dyn Fn(JsValue)> {
147
160
  let state = self.clone();
148
161
  let closure = Closure::new(move |_| {
149
- *state.borrow_mut() = Default::default();
162
+ let mut state = state.borrow_mut();
163
+ let epoch = state.epoch + 1;
164
+ *state = RenderTimerState {
165
+ epoch,
166
+ ..Default::default()
167
+ };
150
168
  });
151
169
 
152
170
  global::document()
@@ -165,6 +183,7 @@ impl Default for RenderTimerState {
165
183
  render_times: Default::default(),
166
184
  total_render_count: Default::default(),
167
185
  start_time,
186
+ epoch: 0,
168
187
  }
169
188
  }
170
189
  }