@perspective-dev/viewer 5.5.0 → 5.6.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 (201) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  43. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  44. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  45. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  46. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  47. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  50. package/dist/wasm/perspective-viewer.d.ts +126 -35
  51. package/dist/wasm/perspective-viewer.js +320 -185
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  54. package/package.json +33 -20
  55. package/src/css/column-selector.css +205 -50
  56. package/src/css/ui/containers/context-menu.css +12 -1
  57. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  58. package/src/css/ui/containers/scroll-panel.css +4 -1
  59. package/src/rust/agent/client/protocol.rs +0 -89
  60. package/src/rust/agent/markdown/mod.rs +0 -238
  61. package/src/rust/agent/preamble.md +1 -0
  62. package/src/rust/agent/tools.rs +2 -16
  63. package/src/rust/components/column_selector/active_column.rs +15 -5
  64. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  65. package/src/rust/components/column_selector/config_selector.rs +34 -6
  66. package/src/rust/components/column_selector/derived_control.rs +229 -0
  67. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  68. package/src/rust/components/column_selector/filter_column.rs +0 -2
  69. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  70. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  71. package/src/rust/components/column_selector/sort_column.rs +4 -3
  72. package/src/rust/components/column_selector.rs +21 -1
  73. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  74. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  75. package/src/rust/components/column_settings_sidebar.rs +25 -19
  76. package/src/rust/components/debug_panel.rs +38 -29
  77. package/src/rust/components/dragdrop_list.rs +8 -9
  78. package/src/rust/components/export_dropdown.rs +6 -0
  79. package/src/rust/components/main_panel/msg.rs +6 -4
  80. package/src/rust/components/main_panel/reconcile.rs +6 -1
  81. package/src/rust/components/main_panel/render.rs +70 -51
  82. package/src/rust/components/main_panel/update.rs +39 -1
  83. package/src/rust/components/main_panel.rs +97 -59
  84. package/src/rust/components/new_panel_menu.rs +69 -1
  85. package/src/rust/components/panel_menu.rs +63 -12
  86. package/src/rust/components/plugin_tab.rs +3 -2
  87. package/src/rust/components/render_warning.rs +2 -1
  88. package/src/rust/components/settings_panel.rs +4 -4
  89. package/src/rust/components/status_bar.rs +80 -21
  90. package/src/rust/components/status_indicator.rs +8 -1
  91. package/src/rust/components/viewer/filters.rs +15 -3
  92. package/src/rust/components/viewer/msg.rs +8 -0
  93. package/src/rust/components/viewer/panels.rs +44 -4
  94. package/src/rust/components/viewer/render.rs +1 -0
  95. package/src/rust/components/viewer/settings.rs +0 -22
  96. package/src/rust/components/viewer/snapshots.rs +2 -2
  97. package/src/rust/components/viewer/wiring.rs +18 -1
  98. package/src/rust/components/viewer.rs +47 -27
  99. package/src/rust/components/window_editor.rs +5 -4
  100. package/src/rust/config/column_config_schema.rs +38 -377
  101. package/src/rust/config/export_method.rs +24 -0
  102. package/src/rust/config/number_string_format.rs +0 -94
  103. package/src/rust/config/options.rs +3 -7
  104. package/src/rust/config/plugin_static_config.rs +0 -42
  105. package/src/rust/config/viewer_config.rs +47 -17
  106. package/src/rust/config/workspace_config.rs +158 -128
  107. package/src/rust/custom_elements/viewer.rs +189 -129
  108. package/src/rust/custom_events.rs +33 -18
  109. package/src/rust/derived.rs +266 -0
  110. package/src/rust/exprtk/tokenize.rs +0 -54
  111. package/src/rust/js/plugin.rs +1 -1
  112. package/src/rust/lib.rs +3 -0
  113. package/src/rust/presentation.rs +23 -4
  114. package/src/rust/queries/column_locator.rs +55 -48
  115. package/src/rust/queries/get_viewer_config.rs +1 -1
  116. package/src/rust/queries/plugin_column_styles.rs +22 -23
  117. package/src/rust/renderer/dispatch.rs +2 -0
  118. package/src/rust/renderer/plugin_config.rs +652 -487
  119. package/src/rust/renderer/props.rs +4 -0
  120. package/src/rust/renderer/registry.rs +2 -6
  121. package/src/rust/renderer/state.rs +205 -0
  122. package/src/rust/renderer.rs +201 -59
  123. package/src/rust/session/metadata.rs +35 -19
  124. package/src/rust/session/op_queue.rs +430 -0
  125. package/src/rust/session/panel_state.rs +327 -0
  126. package/src/rust/session/view_subscription.rs +42 -3
  127. package/src/rust/session.rs +931 -419
  128. package/src/rust/tasks/apply_global_filters.rs +49 -35
  129. package/src/rust/tasks/auto_pause.rs +22 -12
  130. package/src/rust/tasks/copy_export.rs +72 -0
  131. package/src/rust/tasks/create_panel.rs +46 -21
  132. package/src/rust/tasks/derive.rs +312 -0
  133. package/src/rust/tasks/mod.rs +6 -2
  134. package/src/rust/tasks/pipeline.rs +152 -89
  135. package/src/rust/tasks/reset_all.rs +31 -20
  136. package/src/rust/tasks/restore_derived.rs +200 -0
  137. package/src/rust/tasks/restore_panel.rs +153 -141
  138. package/src/rust/tasks/send_column_config.rs +56 -29
  139. package/src/rust/tasks/send_plugin_config.rs +52 -23
  140. package/src/rust/tasks/set_edit_mode.rs +33 -25
  141. package/src/rust/tasks/sync_update_panels.rs +40 -13
  142. package/src/rust/tasks/table_lifecycle.rs +70 -34
  143. package/src/rust/tasks/transactional_restore.rs +387 -0
  144. package/src/rust/tasks/update_theme.rs +45 -6
  145. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  146. package/src/rust/ui/containers/split_panel.rs +0 -41
  147. package/src/rust/ui/context_menu.rs +157 -18
  148. package/src/rust/ui/font_loader.rs +23 -35
  149. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  150. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  151. package/src/rust/ui/form/palette_selector.rs +0 -51
  152. package/src/rust/ui/portal.rs +4 -10
  153. package/src/rust/utils/browser/mod.rs +2 -3
  154. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  155. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  156. package/src/rust/utils/css.rs +0 -360
  157. package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
  158. package/src/rust/utils/local_fonts.rs +0 -36
  159. package/src/rust/utils/mod.rs +4 -3
  160. package/src/rust/utils/modal_position.rs +15 -21
  161. package/src/rust/utils/pending_effects.rs +6 -30
  162. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  163. package/src/rust/utils/pubsub.rs +0 -1
  164. package/src/rust/workspace.rs +187 -140
  165. package/src/svg/derived.svg +6 -0
  166. package/src/themes/icons.css +1 -0
  167. package/src/themes/intl/ar.css +10 -0
  168. package/src/themes/intl/de.css +10 -0
  169. package/src/themes/intl/es.css +10 -0
  170. package/src/themes/intl/fr.css +10 -0
  171. package/src/themes/intl/ja.css +10 -0
  172. package/src/themes/intl/pt.css +10 -0
  173. package/src/themes/intl/zh.css +10 -0
  174. package/src/themes/intl.css +10 -0
  175. package/src/themes/monokai.css +1 -1
  176. package/src/themes/vaporwave.css +1 -1
  177. package/src/ts/extensions.ts +3 -36
  178. package/src/ts/perspective-viewer.ts +1 -1
  179. package/src/ts/plugin.ts +4 -1
  180. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  181. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  182. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  183. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  184. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  185. package/src/ts/ts-rs/TableRef.ts +9 -0
  186. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  187. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  188. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  189. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  190. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  191. package/dist/esm/perspective-viewer.inline.js +0 -21
  192. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  193. package/src/rust/tasks/restore_and_render.rs +0 -107
  194. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  195. package/src/rust/utils/tests/pubsub.rs +0 -95
  196. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  197. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  198. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  199. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  200. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  201. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -15,6 +15,7 @@ use std::rc::Rc;
15
15
  use crate::config::PluginStaticConfig;
16
16
  use crate::renderer::limits::RenderLimits;
17
17
  use crate::renderer::plugin_config::ColumnConfigMap;
18
+ use crate::renderer::state::RenderError;
18
19
  use crate::utils::PtrEqRc;
19
20
 
20
21
  /// Value-semantic snapshot of the renderer state read by components.
@@ -55,6 +56,9 @@ pub struct RendererProps {
55
56
  /// Snapshot of the active plugin's `columns_config` bucket, re-read by
56
57
  /// `Renderer::to_props` on `columns_config_changed` and `plugin_changed`.
57
58
  pub columns_config: PtrEqRc<ColumnConfigMap>,
59
+
60
+ /// The failure of the last draw, which stands until the next commit.
61
+ pub render_error: Option<RenderError>,
58
62
  }
59
63
 
60
64
  impl RendererProps {
@@ -43,7 +43,8 @@ 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();
46
+ // let style =
47
+ // element.unchecked_ref::<HtmlElement>().style();
47
48
  // style.set_property("position", "absolute")?;
48
49
  // style.set_property("top", "0")?;
49
50
  // style.set_property("right", "0")?;
@@ -147,11 +148,6 @@ pub impl LocalKey<Rc<RefCell<Vec<PluginRecord>>>> {
147
148
  });
148
149
  });
149
150
  }
150
-
151
- #[cfg(test)]
152
- fn reset(&'static self) {
153
- self.with(|plugins| plugins.borrow_mut().clear());
154
- }
155
151
  }
156
152
 
157
153
  fn register_default() {
@@ -0,0 +1,205 @@
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
+ use std::cell::RefCell;
14
+ use std::collections::HashMap;
15
+ use std::rc::Rc;
16
+
17
+ use perspective_js::utils::ApiError;
18
+
19
+ use super::plugin_config::PluginScopedConfig;
20
+ use crate::config::{ColumnConfigFieldUpdate, PluginStaticConfig};
21
+
22
+ /// How a panel is drawn, as one immutable value replaced whole.
23
+ #[derive(Clone, Default)]
24
+ pub struct RendererState {
25
+ /// The selected plugin, or `None` until the first run selects one.
26
+ pub plugin: Option<PluginRef>,
27
+
28
+ /// Every plugin's own `plugin_config` and `columns_config`, keyed by
29
+ /// plugin name.
30
+ pub buckets: Rc<HashMap<String, PluginScopedConfig>>,
31
+
32
+ /// This panel's concrete theme name.
33
+ pub theme: Option<String>,
34
+ }
35
+
36
+ /// A plugin selection: its index in the plugin store and its static config.
37
+ #[derive(Clone)]
38
+ pub struct PluginRef {
39
+ pub idx: usize,
40
+ pub static_config: Rc<PluginStaticConfig>,
41
+ }
42
+
43
+ impl RendererState {
44
+ pub fn with_theme(&self, theme: Option<String>) -> Self {
45
+ Self {
46
+ theme,
47
+ ..self.clone()
48
+ }
49
+ }
50
+
51
+ pub fn with_plugin(&self, plugin: PluginRef) -> Self {
52
+ Self {
53
+ plugin: Some(plugin),
54
+ ..self.clone()
55
+ }
56
+ }
57
+
58
+ /// This state with no plugin selected and every bucket forgotten.
59
+ pub fn without_plugins(&self) -> Self {
60
+ Self {
61
+ plugin: None,
62
+ buckets: Rc::default(),
63
+ ..self.clone()
64
+ }
65
+ }
66
+
67
+ /// The named plugin's bucket, empty when it has never been written.
68
+ pub fn bucket(&self, name: &str) -> PluginScopedConfig {
69
+ self.buckets.get(name).cloned().unwrap_or_default()
70
+ }
71
+
72
+ /// This state with the named plugin's bucket replaced.
73
+ pub fn with_bucket(&self, name: &str, bucket: PluginScopedConfig) -> Self {
74
+ let mut buckets = (*self.buckets).clone();
75
+ buckets.insert(name.to_owned(), bucket);
76
+ Self {
77
+ buckets: Rc::new(buckets),
78
+ ..self.clone()
79
+ }
80
+ }
81
+ }
82
+
83
+ /// A failure to draw a valid committed state.
84
+ #[derive(Clone)]
85
+ pub struct RenderError(pub ApiError);
86
+
87
+ impl PartialEq for RenderError {
88
+ fn eq(&self, other: &Self) -> bool {
89
+ self.0.to_string() == other.0.to_string()
90
+ }
91
+ }
92
+
93
+ impl std::fmt::Debug for RenderError {
94
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95
+ f.debug_tuple("RenderError")
96
+ .field(&self.0.to_string())
97
+ .finish()
98
+ }
99
+ }
100
+
101
+ /// How the last draw of the committed state went, which any commit resets.
102
+ #[derive(Clone, Default)]
103
+ pub enum RenderOutcome {
104
+ #[default]
105
+ Never,
106
+ Ok,
107
+ Failed(RenderError),
108
+ }
109
+
110
+ /// A UI edit of the renderer's state that is queued but not yet committed.
111
+ #[derive(Clone)]
112
+ pub enum StagedEdit {
113
+ /// One plugin-level settings field of the selected plugin.
114
+ PluginField(ColumnConfigFieldUpdate),
115
+
116
+ /// One style field of one column, for the selected plugin.
117
+ ColumnField {
118
+ column: String,
119
+ update: ColumnConfigFieldUpdate,
120
+ },
121
+
122
+ /// Keys merged into the selected plugin's `plugin_config`.
123
+ PluginConfig(serde_json::Map<String, serde_json::Value>),
124
+ Theme(Option<String>),
125
+ }
126
+
127
+ /// The renderer's staged edits, in the order they were made.
128
+ #[derive(Default)]
129
+ pub(super) struct StagedEdits {
130
+ next_id: std::cell::Cell<u64>,
131
+ edits: RefCell<Vec<(u64, StagedEdit)>>,
132
+ }
133
+
134
+ impl StagedEdits {
135
+ pub fn stage(self: &Rc<Self>, edit: StagedEdit) -> Staged {
136
+ let id = self.next_id.get();
137
+ self.next_id.set(id + 1);
138
+ self.edits.borrow_mut().push((id, edit));
139
+ Staged {
140
+ id,
141
+ edits: Rc::downgrade(self),
142
+ }
143
+ }
144
+
145
+ pub fn pending(&self) -> Vec<StagedEdit> {
146
+ self.edits.borrow().iter().map(|x| x.1.clone()).collect()
147
+ }
148
+ }
149
+
150
+ /// Keeps one [`StagedEdit`] visible to the UI until its op leaves the queue.
151
+ pub struct Staged {
152
+ id: u64,
153
+ edits: std::rc::Weak<StagedEdits>,
154
+ }
155
+
156
+ impl Drop for Staged {
157
+ fn drop(&mut self) {
158
+ if let Some(edits) = self.edits.upgrade() {
159
+ edits.edits.borrow_mut().retain(|x| x.0 != self.id);
160
+ }
161
+ }
162
+ }
163
+
164
+ /// `bucket` with every pending bucket edit applied, in the order made.
165
+ pub(super) fn project_bucket(
166
+ mut bucket: PluginScopedConfig,
167
+ pending: Vec<StagedEdit>,
168
+ ) -> PluginScopedConfig {
169
+ for edit in pending {
170
+ match edit {
171
+ StagedEdit::PluginField(update) => {
172
+ for key in &update.keys {
173
+ match update.value.get(key) {
174
+ Some(value) => {
175
+ bucket.plugin.insert(key.clone(), value.clone());
176
+ },
177
+ None => {
178
+ bucket.plugin.remove(key);
179
+ },
180
+ }
181
+ }
182
+ },
183
+ StagedEdit::ColumnField { column, update } => {
184
+ let entry = bucket.columns.entry(column.clone()).or_default();
185
+ for key in &update.keys {
186
+ entry.remove(key);
187
+ }
188
+
189
+ for (key, value) in update.value {
190
+ if update.keys.contains(&key) {
191
+ entry.insert(key, value);
192
+ }
193
+ }
194
+
195
+ if entry.is_empty() {
196
+ bucket.columns.remove(&column);
197
+ }
198
+ },
199
+ StagedEdit::PluginConfig(map) => bucket.plugin.extend(map),
200
+ StagedEdit::Theme(_) => {},
201
+ }
202
+ }
203
+
204
+ bucket
205
+ }
@@ -25,6 +25,7 @@ mod plugin_store;
25
25
  mod props;
26
26
  mod registry;
27
27
  mod render_timer;
28
+ mod state;
28
29
 
29
30
  use std::cell::{Cell, RefCell};
30
31
  use std::collections::HashMap;
@@ -32,7 +33,7 @@ use std::ops::Deref;
32
33
  use std::rc::Rc;
33
34
 
34
35
  use perspective_client::ViewWindow;
35
- use perspective_js::utils::{ApiFuture, ApiResult, JsValueSerdeExt};
36
+ use perspective_js::utils::{ApiError, ApiFuture, ApiResult, JsValueSerdeExt};
36
37
  use wasm_bindgen::prelude::*;
37
38
  use web_sys::*;
38
39
  use yew::html::ImplicitClone;
@@ -40,11 +41,16 @@ use yew::prelude::*;
40
41
 
41
42
  use self::activate::*;
42
43
  pub use self::limits::RenderLimits;
43
- pub use self::plugin_config::{ColumnConfigMap, PluginScopedConfig};
44
+ pub use self::plugin_config::{
45
+ ColumnConfigMap, ColumnRole, PluginScopedConfig, PreparedRenderer, ValidatedColumnsConfig,
46
+ ValidatedPluginConfig,
47
+ };
44
48
  use self::plugin_store::*;
45
49
  pub use self::props::RendererProps;
46
50
  pub use self::registry::*;
47
51
  use self::render_timer::*;
52
+ pub use self::state::{PluginRef, RenderError, RendererState, Staged, StagedEdit};
53
+ use self::state::{RenderOutcome, StagedEdits, project_bucket};
48
54
  use crate::config::*;
49
55
  use crate::js::plugin::*;
50
56
  use crate::utils::*;
@@ -97,6 +103,13 @@ impl Drop for ContextPin {
97
103
 
98
104
  /// Immutable state
99
105
  pub struct RendererData {
106
+ state: RefCell<Rc<RendererState>>,
107
+ outcome: RefCell<RenderOutcome>,
108
+
109
+ /// Injected callback from the root component, fired when a render failure
110
+ /// is raised or cleared.
111
+ pub on_render_failure_changed: RefCell<Option<Callback<()>>>,
112
+ staged: Rc<StagedEdits>,
100
113
  plugin_data: RefCell<RendererMutData>,
101
114
  draw_lock: DebounceMutex,
102
115
  pub plugin_changed: PubSub<JsPerspectiveViewerPlugin>,
@@ -151,20 +164,12 @@ pub struct RendererData {
151
164
  /// coexist there.
152
165
  slot_name: RefCell<Option<String>>,
153
166
 
154
- /// This panel's theme name — CONCRETE, resolved once at creation from
155
- /// the config's `theme`, else the host's, else the registry default.
156
- /// There is no "unthemed panel" that resolves against live element
157
- /// state at draw time: a registry re-ordering must never repaint a
158
- /// panel, so nothing but an explicit write may change this. `None` only
159
- /// while no themes exist at all.
160
- theme: RefCell<Option<String>>,
161
-
162
167
  /// Whether the active plugin has completed a draw. An EXPLICIT flag —
163
168
  /// not inferred from DOM connectedness — because plugin elements may be
164
169
  /// mounted eagerly (at panel creation / draw start, before the view
165
170
  /// query resolves), so "in the DOM" no longer implies "has rendered".
166
171
  /// Set by a successful `draw_view`; cleared on plugin swap
167
- /// (`commit_plugin_idx`), `dispose` and `delete`.
172
+ /// (`activate_committed_plugin`), `dispose` and `delete`.
168
173
  has_drawn: Cell<bool>,
169
174
 
170
175
  /// The effective theme stamped at the active plugin's last `--psp-*`
@@ -210,12 +215,9 @@ pub struct RendererData {
210
215
  /// Mutable state
211
216
  pub struct RendererMutData {
212
217
  viewer_elem: HtmlElement,
213
- metadata: Rc<PluginStaticConfig>,
214
218
  plugin_store: PluginStore,
215
- plugins_idx: Option<usize>,
216
219
  timer: MovingWindowRenderTimer,
217
220
  selection: Option<ViewWindow>,
218
- plugin_states: HashMap<String, PluginScopedConfig>,
219
221
  }
220
222
 
221
223
  /// The state object responsible for the active [`JsPerspectiveViewerPlugin`].
@@ -250,14 +252,15 @@ impl Renderer {
250
252
  pub fn new(viewer_elem: &HtmlElement) -> Self {
251
253
  let draw_lock = DebounceMutex::default();
252
254
  Self(Rc::new(RendererData {
255
+ state: RefCell::default(),
256
+ outcome: RefCell::default(),
257
+ on_render_failure_changed: RefCell::default(),
258
+ staged: Rc::default(),
253
259
  plugin_data: RefCell::new(RendererMutData {
254
260
  viewer_elem: viewer_elem.clone(),
255
- metadata: Rc::new(PluginStaticConfig::default()),
256
261
  plugin_store: PluginStore::default(),
257
- plugins_idx: None,
258
262
  selection: None,
259
263
  timer: MovingWindowRenderTimer::default(),
260
- plugin_states: HashMap::default(),
261
264
  }),
262
265
  geometry_slot: draw_lock.slot(),
263
266
  geometry_cmd: Cell::new(None),
@@ -274,7 +277,6 @@ impl Renderer {
274
277
  presized_box: Cell::new(None),
275
278
  on_render_limits_changed: Default::default(),
276
279
  slot_name: Default::default(),
277
- theme: Default::default(),
278
280
  has_drawn: Cell::new(false),
279
281
  captured_theme: Default::default(),
280
282
  cached_context: Default::default(),
@@ -297,26 +299,83 @@ impl Renderer {
297
299
  self.0.slot_name.borrow().clone()
298
300
  }
299
301
 
300
- /// Set this panel's theme name. Callers must pass a CONCRETE name —
301
- /// resolve "the default" through `Presentation` before calling, never by
302
- /// leaving this empty.
303
- pub fn set_theme(&self, name: Option<String>) {
304
- *self.0.theme.borrow_mut() = name;
302
+ /// Write this panel's concrete theme name.
303
+ pub fn commit_theme(&self, name: Option<String>) {
304
+ self.swap(self.state().with_theme(name));
305
305
  }
306
306
 
307
- /// This panel's theme name.
308
- pub fn theme(&self) -> Option<String> {
309
- self.0.theme.borrow().clone()
307
+ /// The committed state, which is what a running step reads.
308
+ pub fn state(&self) -> Rc<RendererState> {
309
+ self.0.state.borrow().clone()
310
+ }
311
+
312
+ pub(crate) fn swap(&self, next: RendererState) {
313
+ *self.0.state.borrow_mut() = Rc::new(next);
314
+ self.set_outcome(RenderOutcome::Never);
315
+ }
316
+
317
+ /// The failure of the last draw, which stands until the next commit.
318
+ pub fn failure(&self) -> Option<RenderError> {
319
+ match &*self.0.outcome.borrow() {
320
+ RenderOutcome::Failed(error) => Some(error.clone()),
321
+ _ => None,
322
+ }
323
+ }
324
+
325
+ /// Record that the draw of the committed state failed.
326
+ pub fn fail(&self, error: ApiError) -> ApiResult<()> {
327
+ self.set_outcome(RenderOutcome::Failed(RenderError(error.clone())));
328
+ Err(error)
329
+ }
330
+
331
+ /// Record that the draw of the committed state landed.
332
+ pub fn landed(&self) {
333
+ self.set_outcome(RenderOutcome::Ok);
334
+ }
335
+
336
+ fn set_outcome(&self, next: RenderOutcome) {
337
+ let was_failed = self.failure().is_some();
338
+ let is_failed = matches!(next, RenderOutcome::Failed(_));
339
+ *self.0.outcome.borrow_mut() = next;
340
+ if was_failed != is_failed
341
+ && let Some(cb) = self.0.on_render_failure_changed.borrow().as_ref()
342
+ {
343
+ cb.emit(());
344
+ }
345
+ }
346
+
347
+ /// Show `edit` to the UI until the returned guard is dropped.
348
+ pub fn stage(&self, edit: StagedEdit) -> Staged {
349
+ self.0.staged.stage(edit)
350
+ }
351
+
352
+ fn projected_bucket(&self, name: &str) -> PluginScopedConfig {
353
+ project_bucket(self.state().bucket(name), self.0.staged.pending())
310
354
  }
311
355
 
312
- /// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] — the
313
- /// shared "stamp-with-commit" head of every per-panel theme mutation
314
- /// site.
315
- pub fn set_theme_stamped(&self, theme: Option<String>) {
316
- self.set_theme(theme);
356
+ /// [`Self::commit_theme`] plus a synchronous [`Self::stamp_theme`].
357
+ pub fn commit_theme_stamped(&self, theme: Option<String>) {
358
+ self.commit_theme(theme);
317
359
  self.stamp_theme(None);
318
360
  }
319
361
 
362
+ /// This panel's theme name, as the UI sees it: the committed theme, or the
363
+ /// latest pending pick.
364
+ pub fn theme(&self) -> Option<String> {
365
+ let pending = self
366
+ .0
367
+ .staged
368
+ .pending()
369
+ .into_iter()
370
+ .filter_map(|edit| match edit {
371
+ StagedEdit::Theme(theme) => Some(theme),
372
+ _ => None,
373
+ })
374
+ .next_back();
375
+
376
+ pending.unwrap_or_else(|| self.state().theme.clone())
377
+ }
378
+
320
379
  /// Whether the active plugin's captured `--psp-*` CSS is STALE — the
321
380
  /// effective theme differs from the one stamped at the plugin's last
322
381
  /// CSS capture (first paint / last restyle).
@@ -376,17 +435,24 @@ impl Renderer {
376
435
  plugin.delete();
377
436
  }
378
437
  self.plugin_data.borrow().viewer_elem.set_inner_text("");
379
- let new_state = Self::new(&self.plugin_data.borrow().viewer_elem);
380
- std::mem::swap(
381
- &mut *self.plugin_data.borrow_mut(),
382
- &mut *new_state.plugin_data.borrow_mut(),
383
- );
438
+ let viewer_elem = self.plugin_data.borrow().viewer_elem.clone();
439
+ *self.plugin_data.borrow_mut() = RendererMutData {
440
+ viewer_elem,
441
+ plugin_store: PluginStore::default(),
442
+ selection: None,
443
+ timer: MovingWindowRenderTimer::default(),
444
+ };
384
445
 
446
+ self.swap(self.state().without_plugins());
385
447
  Ok(())
386
448
  }
387
449
 
388
450
  pub fn metadata(&self) -> Rc<PluginStaticConfig> {
389
- self.borrow().metadata.clone()
451
+ self.state()
452
+ .plugin
453
+ .as_ref()
454
+ .map(|plugin| plugin.static_config.clone())
455
+ .unwrap_or_default()
390
456
  }
391
457
 
392
458
  pub fn is_chart(&self) -> bool {
@@ -438,9 +504,9 @@ impl Renderer {
438
504
  /// permanently pins the per-renderer store to the built-in `Debug`
439
505
  /// (init-order race, surfaces on Safari).
440
506
  pub fn active_plugin(&self) -> Option<JsPerspectiveViewerPlugin> {
441
- // Bail on `plugins_idx` BEFORE touching `plugin_store`, so an unselected
442
- // renderer never snapshots the registry.
443
- let idx = self.0.borrow().plugins_idx?;
507
+ // Bail on `plugins_idx` BEFORE touching `plugin_store`, so an
508
+ // unselected renderer never snapshots the registry.
509
+ let idx = self.selected_idx()?;
444
510
  self.0.borrow_mut().plugin_store.plugins().get(idx).cloned()
445
511
  }
446
512
 
@@ -488,6 +554,30 @@ impl Renderer {
488
554
  /// empty panel frame. NOTE eager mounting is exactly why
489
555
  /// [`Self::is_plugin_activated`] must be an explicit has-drawn flag, not
490
556
  /// DOM-connectedness.
557
+ /// Rename this renderer's slot (and its mounted light-DOM elements'
558
+ /// `slot` attributes) ahead of an eject whose slot name a replacement
559
+ /// panel reuses, so the async [`Self::dispose`] sweep cannot remove the
560
+ /// successor's elements.
561
+ pub fn retire_slot(&self) {
562
+ let Some(slot) = self.slot_name() else {
563
+ return;
564
+ };
565
+
566
+ let viewer = self.0.borrow().viewer_elem.clone();
567
+ let slots = [slot.clone(), format!("statusbar-extra-{slot}")];
568
+ let children = viewer.children();
569
+ for idx in 0..children.length() {
570
+ if let Some(el) = children.item(idx)
571
+ && let Some(s) = el.get_attribute("slot")
572
+ && slots.contains(&s)
573
+ {
574
+ let _ = el.set_attribute("slot", &format!("{s}-retired"));
575
+ }
576
+ }
577
+
578
+ self.set_slot_name(&format!("{slot}-retired"));
579
+ }
580
+
491
581
  pub fn mount_active_plugin(&self) -> ApiResult<()> {
492
582
  if let Some(plugin) = self.active_plugin() {
493
583
  let viewer_elem = self.0.borrow().viewer_elem.clone();
@@ -556,7 +646,7 @@ impl Renderer {
556
646
 
557
647
  let idx = self.find_plugin_idx(name)?;
558
648
  let changed = !matches!(
559
- self.0.borrow().plugins_idx,
649
+ self.selected_idx(),
560
650
  Some(selected_idx) if selected_idx == idx
561
651
  );
562
652
 
@@ -574,6 +664,24 @@ impl Renderer {
574
664
  }
575
665
  }
576
666
 
667
+ /// Reject a [`PluginUpdate`] naming a plugin that is not registered.
668
+ pub fn check_plugin_update(&self, update: &PluginUpdate) -> ApiResult<()> {
669
+ if let PluginUpdate::Update(name) = update
670
+ && self.find_plugin_idx(name).is_none()
671
+ {
672
+ let known = self
673
+ .get_all_plugin_configs()
674
+ .iter()
675
+ .map(|c| format!("\"{}\"", c.name))
676
+ .collect::<Vec<_>>()
677
+ .join(", ");
678
+
679
+ return Err(format!("Unknown plugin \"{name}\"; expected one of {known}").into());
680
+ }
681
+
682
+ Ok(())
683
+ }
684
+
577
685
  /// Commit a plugin selection previously resolved by
578
686
  /// [`Self::resolve_plugin_update`]. COMMAND — call only from inside a
579
687
  /// locked draw task, so the swap lands atomically with the view rebuild
@@ -586,7 +694,7 @@ impl Renderer {
586
694
  let idx = match idx {
587
695
  Some(idx) => idx,
588
696
  None => {
589
- if self.0.borrow().plugins_idx.is_none() {
697
+ if self.selected_idx().is_none() {
590
698
  let name = PLUGIN_REGISTRY.default_plugin_name();
591
699
  let idx = self
592
700
  .find_plugin_idx(&name)
@@ -600,7 +708,7 @@ impl Renderer {
600
708
  };
601
709
 
602
710
  let changed = !matches!(
603
- self.0.borrow().plugins_idx,
711
+ self.selected_idx(),
604
712
  Some(selected_idx) if selected_idx == idx
605
713
  );
606
714
 
@@ -616,12 +724,6 @@ impl Renderer {
616
724
  /// reset the per-plugin render-warning flag, and fire
617
725
  /// `plugin_changed`.
618
726
  fn commit_plugin_idx(&self, idx: usize) -> ApiResult<()> {
619
- // The newly-selected plugin element has not drawn (a swap keeps the
620
- // OLD plugin mounted until the new one's draw lands) — and has
621
- // captured no CSS.
622
- self.0.has_drawn.set(false);
623
- self.0.captured_theme.borrow_mut().take();
624
- self.borrow_mut().plugins_idx = Some(idx);
625
727
  let config = self
626
728
  .0
627
729
  .borrow_mut()
@@ -631,22 +733,46 @@ impl Renderer {
631
733
  .cloned()
632
734
  .ok_or("No Plugin")?;
633
735
 
634
- self.borrow_mut().metadata = config.clone();
736
+ self.swap(self.state().with_plugin(PluginRef {
737
+ idx,
738
+ static_config: config,
739
+ }));
740
+
741
+ self.activate_committed_plugin()
742
+ }
743
+
744
+ /// The `static_config` of the plugin at store index `idx`, for a
745
+ /// [`PluginRef`] committed by a transaction rather than by
746
+ /// [`Self::commit_plugin`].
747
+ pub fn plugin_ref(&self, idx: usize) -> ApiResult<PluginRef> {
748
+ let static_config = self
749
+ .0
750
+ .borrow_mut()
751
+ .plugin_store
752
+ .plugin_configs()
753
+ .get(idx)
754
+ .cloned()
755
+ .ok_or("No Plugin")?;
756
+
757
+ Ok(PluginRef { idx, static_config })
758
+ }
759
+
760
+ /// Bring the plugin ELEMENT in line with a just-committed selection: the
761
+ /// newly-selected element has drawn nothing and captured no CSS, takes its
762
+ /// own stored bucket, and is announced.
763
+ pub fn activate_committed_plugin(&self) -> ApiResult<()> {
764
+ // The newly-selected plugin element has not drawn (a swap keeps the
765
+ // OLD plugin mounted until the new one's draw lands) — and has
766
+ // captured no CSS.
767
+ self.0.has_drawn.set(false);
768
+ self.0.captured_theme.borrow_mut().take();
635
769
  self.0.render_warning.set(true);
636
- // `commit_plugin_idx` is called *by* the selection path, so it must use
637
- // the pure query (never `ensure_plugin_selected`, which would recurse
638
- // through `commit_plugin`). `plugins_idx` was just set above.
639
770
  let plugin: JsPerspectiveViewerPlugin = self.active_plugin().ok_or("No Plugin")?;
640
771
 
641
772
  // Push the newly-activated plugin's stored bucket through
642
773
  // `plugin.restore` so the swap immediately reflects any
643
774
  // viewer-owned per-column and plugin-level config.
644
- let bucket = self
645
- .borrow()
646
- .plugin_states
647
- .get(&config.name)
648
- .cloned()
649
- .unwrap_or_default();
775
+ let bucket = self.state().bucket(&self.metadata().name);
650
776
  let token = JsValue::from_serde_ext(&bucket.plugin).unwrap_or(JsValue::NULL);
651
777
  if let Err(e) = plugin.restore(&token, Some(&bucket.columns)) {
652
778
  tracing::warn!("plugin.restore on swap failed: {:?}", e);
@@ -656,6 +782,20 @@ impl Renderer {
656
782
  Ok(())
657
783
  }
658
784
 
785
+ /// The committed plugin selection's index into the plugin store.
786
+ pub fn committed_plugin_idx(&self) -> Option<usize> {
787
+ self.selected_idx()
788
+ }
789
+
790
+ /// The COMMITTED panel theme, pending theme edits excluded.
791
+ pub fn committed_theme(&self) -> Option<String> {
792
+ self.state().theme.clone()
793
+ }
794
+
795
+ fn selected_idx(&self) -> Option<usize> {
796
+ self.state().plugin.as_ref().map(|plugin| plugin.idx)
797
+ }
798
+
659
799
  pub fn render_timer(&self) -> MovingWindowRenderTimer {
660
800
  self.0.borrow().timer.clone()
661
801
  }
@@ -710,6 +850,7 @@ impl Renderer {
710
850
  is_chart,
711
851
  plugin_config,
712
852
  columns_config,
853
+ render_error: self.failure(),
713
854
  }
714
855
  } else {
715
856
  RendererProps {
@@ -720,6 +861,7 @@ impl Renderer {
720
861
  is_chart: false,
721
862
  plugin_config: PtrEqRc::default(),
722
863
  columns_config: PtrEqRc::default(),
864
+ render_error: self.failure(),
723
865
  }
724
866
  }
725
867
  }