@perspective-dev/viewer 5.0.0 → 5.2.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 (213) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  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/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +2 -0
  26. package/dist/esm/perspective-viewer.inline.js +8 -8
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/providers.d.ts +79 -0
  31. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  32. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  33. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  35. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  36. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  38. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  39. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  40. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  41. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  42. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  43. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  44. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  45. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +43 -1
  46. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  47. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  48. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  49. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  50. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  51. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  52. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  53. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  54. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  55. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  56. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  57. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  59. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  60. package/dist/esm/ts-rs/ViewerConfig.d.ts +43 -1
  61. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  62. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +72 -0
  63. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  64. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +47 -0
  66. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +7 -6
  68. package/dist/wasm/perspective-viewer.d.ts +222 -26
  69. package/dist/wasm/perspective-viewer.js +378 -125
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -15
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +9 -0
  75. package/src/css/column-settings-panel.css +67 -2
  76. package/src/css/column-style.css +4 -3
  77. package/src/css/config-selector.css +60 -4
  78. package/src/css/containers/context-menu.css +12 -6
  79. package/src/css/containers/tabs.css +2 -2
  80. package/src/css/form/code-editor.css +2 -3
  81. package/src/css/form/mirrored-textarea.css +81 -0
  82. package/src/css/viewer.css +336 -0
  83. package/src/rust/agent/client/mod.rs +19 -0
  84. package/src/rust/agent/client/protocol.rs +320 -0
  85. package/src/rust/agent/client/transport.rs +287 -0
  86. package/src/rust/agent/client/turn.rs +174 -0
  87. package/src/rust/agent/config.rs +159 -0
  88. package/src/rust/agent/docs.rs +293 -0
  89. package/src/rust/agent/markdown/block.rs +509 -0
  90. package/src/rust/agent/markdown/entity.rs +52 -0
  91. package/src/rust/agent/markdown/events.rs +71 -0
  92. package/src/rust/agent/markdown/inline.rs +497 -0
  93. package/src/rust/agent/markdown/mod.rs +466 -0
  94. package/src/rust/agent/mod.rs +23 -0
  95. package/src/rust/agent/preamble.md +16 -0
  96. package/src/rust/agent/runtime.rs +201 -0
  97. package/src/rust/agent/slot.rs +236 -0
  98. package/src/rust/agent/tools.rs +1110 -0
  99. package/src/rust/components/chat_panel.rs +289 -0
  100. package/src/rust/components/column_selector/active_column.rs +21 -27
  101. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  102. package/src/rust/components/column_selector/config_selector.rs +83 -8
  103. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  104. package/src/rust/components/column_selector/inactive_column.rs +8 -1
  105. package/src/rust/components/column_selector.rs +27 -3
  106. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  107. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  108. package/src/rust/components/column_settings_sidebar.rs +163 -16
  109. package/src/rust/components/containers/dragdrop_list.rs +110 -3
  110. package/src/rust/components/containers/sidebar.rs +99 -22
  111. package/src/rust/components/containers/split_panel.rs +6 -0
  112. package/src/rust/components/editable_header.rs +2 -0
  113. package/src/rust/components/form/code_editor.rs +14 -23
  114. package/src/rust/components/form/debug.rs +8 -5
  115. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  116. package/src/rust/components/form/mod.rs +1 -0
  117. package/src/rust/components/main_panel.rs +5 -0
  118. package/src/rust/components/mod.rs +3 -0
  119. package/src/rust/components/panel_menu.rs +7 -0
  120. package/src/rust/components/panel_tab.rs +5 -0
  121. package/src/rust/components/portal.rs +7 -0
  122. package/src/rust/components/settings_panel.rs +80 -2
  123. package/src/rust/components/viewer/msg.rs +2 -0
  124. package/src/rust/components/viewer/panels.rs +9 -14
  125. package/src/rust/components/viewer/render.rs +14 -0
  126. package/src/rust/components/viewer/settings.rs +33 -1
  127. package/src/rust/components/viewer.rs +2 -0
  128. package/src/rust/components/window_editor.rs +985 -0
  129. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  130. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  131. package/src/rust/config/number_string_format.rs +33 -0
  132. package/src/rust/config/options.rs +18 -0
  133. package/src/rust/config/plugin_static_config.rs +120 -5
  134. package/src/rust/config/viewer_config.rs +220 -1
  135. package/src/rust/config/workspace_config.rs +7 -6
  136. package/src/rust/custom_elements/viewer.rs +209 -18
  137. package/src/rust/custom_events.rs +29 -0
  138. package/src/rust/lib.rs +17 -1
  139. package/src/rust/presentation/column_locator.rs +16 -1
  140. package/src/rust/presentation.rs +8 -0
  141. package/src/rust/queries/columns_iter_set.rs +7 -1
  142. package/src/rust/queries/get_viewer_config.rs +5 -1
  143. package/src/rust/renderer/dispatch.rs +85 -58
  144. package/src/rust/renderer.rs +34 -1
  145. package/src/rust/session/column_defaults_update.rs +18 -0
  146. package/src/rust/session/drag_drop_update.rs +18 -0
  147. package/src/rust/session/metadata.rs +64 -9
  148. package/src/rust/session/replace_expression_update.rs +2 -0
  149. package/src/rust/session.rs +7 -5
  150. package/src/rust/tasks/create_panel.rs +18 -8
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/mod.rs +2 -0
  153. package/src/rust/tasks/restore_panel.rs +34 -17
  154. package/src/rust/tasks/sync_update_panels.rs +7 -23
  155. package/src/rust/utils/browser/dragdrop.rs +12 -0
  156. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  157. package/src/rust/utils/debounce.rs +76 -27
  158. package/src/rust/workspace.rs +112 -20
  159. package/src/svg/pin-icon.svg +5 -0
  160. package/src/themes/icons.css +1 -0
  161. package/src/themes/intl/de.css +23 -0
  162. package/src/themes/intl/es.css +23 -0
  163. package/src/themes/intl/fr.css +23 -0
  164. package/src/themes/intl/ja.css +23 -0
  165. package/src/themes/intl/pt.css +23 -0
  166. package/src/themes/intl/zh.css +23 -0
  167. package/src/themes/intl.css +26 -1
  168. package/src/ts/column-format.ts +67 -46
  169. package/src/ts/perspective-viewer.ts +2 -0
  170. package/src/ts/providers.ts +96 -0
  171. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  172. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  173. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  174. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  175. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  176. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  177. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  178. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  179. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  180. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  181. package/src/ts/ts-rs/FormatMode.ts +3 -0
  182. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  183. package/src/ts/ts-rs/Notation.ts +11 -0
  184. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  185. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -1
  186. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  187. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  188. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  189. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  190. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  191. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  192. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  193. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  194. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  195. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  196. package/src/ts/ts-rs/Unit.ts +3 -0
  197. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  198. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  199. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  200. package/src/ts/ts-rs/ViewerConfig.ts +48 -1
  201. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  202. package/src/ts/ts-rs/ViewerConfigUpdate.ts +81 -2
  203. package/src/ts/ts-rs/WindowSort.ts +9 -0
  204. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  205. package/src/ts/ts-rs/WindowSpec.ts +47 -0
  206. package/src/ts/ts-rs/Windows.ts +10 -0
  207. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -6
  208. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline0.js +0 -0
  209. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline1.js +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline2.js +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline3.js +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline4.js +0 -0
  213. /package/dist/wasm/snippets/{perspective-viewer-daeef4ceb066e6c7 → perspective-viewer-02e461f3d7062b26}/inline5.js +0 -0
@@ -25,7 +25,7 @@ use web_sys::*;
25
25
 
26
26
  use super::activate::*;
27
27
  use super::limits::*;
28
- use super::{ContextPin, RenderContext, Renderer, SUBPIXEL_EPSILON};
28
+ use super::{ContextPin, GeometryCmd, RenderContext, Renderer, SUBPIXEL_EPSILON};
29
29
  use crate::js::plugin::*;
30
30
  use crate::session::FreshView;
31
31
  use crate::utils::*;
@@ -133,19 +133,8 @@ impl Renderer {
133
133
  }
134
134
 
135
135
  pub async fn resize(&self) -> ApiResult<()> {
136
- let draw_mutex = self.draw_lock();
137
- let timer = self.render_timer();
138
- draw_mutex
139
- .debounce_with(|_guard| async move {
140
- set_timeout(timer.get_throttle()).await?;
141
- let Some(jsplugin) = self.active_plugin() else {
142
- return Ok(());
143
- };
144
- self.stamp_active(&jsplugin);
145
- jsplugin.resize().await?;
146
- Ok(())
147
- })
148
- .await
136
+ self.0.geometry_cmd.set(Some(GeometryCmd::Measure));
137
+ self.geometry_task().await.map(|_| ())
149
138
  }
150
139
 
151
140
  /// Public one-shot resize to explicit dimensions (`viewer.resize({
@@ -190,62 +179,100 @@ impl Renderer {
190
179
  width: f64,
191
180
  height: f64,
192
181
  ) -> ApiResult<Option<js_sys::Function>> {
193
- let draw_mutex = self.draw_lock();
194
- draw_mutex
182
+ self.0.geometry_cmd.set(Some(GeometryCmd::Presize {
183
+ translate,
184
+ width,
185
+ height,
186
+ }));
187
+
188
+ self.geometry_task().await
189
+ }
190
+
191
+ /// Run the LATEST queued [`GeometryCmd`] on the geometry debounce slot.
192
+ async fn geometry_task(&self) -> ApiResult<Option<js_sys::Function>> {
193
+ let timer = self.render_timer();
194
+ self.0
195
+ .geometry_slot
196
+ .clone()
195
197
  .debounce_with(|_guard| async move {
196
- let Some(plugin) = self.active_plugin() else {
198
+ let Some(cmd) = self.0.geometry_cmd.take() else {
197
199
  return Ok(None);
198
200
  };
199
201
 
200
- self.stamp_active(&plugin);
201
- self.clear_presize()?;
202
- let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
203
- let rect = main_panel.get_bounding_client_rect();
204
- let size_changed = (height - rect.height()).abs() > SUBPIXEL_EPSILON
205
- || (width - rect.width()).abs() > SUBPIXEL_EPSILON;
206
-
207
- let (dx, dy) = translate.unwrap_or((0.0, 0.0));
208
- let moved = dx.abs() > SUBPIXEL_EPSILON || dy.abs() > SUBPIXEL_EPSILON;
209
- if !size_changed && !moved {
210
- return Ok(None);
202
+ match cmd {
203
+ GeometryCmd::Measure => {
204
+ set_timeout(timer.get_throttle()).await?;
205
+ let Some(jsplugin) = self.active_plugin() else {
206
+ return Ok(None);
207
+ };
208
+
209
+ self.stamp_active(&jsplugin);
210
+ jsplugin.resize().await?;
211
+ Ok(None)
212
+ },
213
+ GeometryCmd::Presize {
214
+ translate,
215
+ width,
216
+ height,
217
+ } => self.run_presize(translate, width, height).await,
211
218
  }
219
+ })
220
+ .await
221
+ }
222
+
223
+ async fn run_presize(
224
+ &self,
225
+ translate: Option<(f64, f64)>,
226
+ width: f64,
227
+ height: f64,
228
+ ) -> ApiResult<Option<js_sys::Function>> {
229
+ let Some(plugin) = self.active_plugin() else {
230
+ return Ok(None);
231
+ };
212
232
 
213
- if plugin.capabilities().presize {
214
- if size_changed {
215
- return plugin.presize(width, height).await;
216
- }
233
+ self.stamp_active(&plugin);
234
+ self.clear_presize()?;
235
+ if plugin.capabilities().presize {
236
+ return plugin.presize(width, height).await;
237
+ }
217
238
 
218
- return Ok(None);
219
- }
239
+ let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
240
+ let rect = main_panel.get_bounding_client_rect();
241
+ let size_changed = (height - rect.height()).abs() > SUBPIXEL_EPSILON
242
+ || (width - rect.width()).abs() > SUBPIXEL_EPSILON;
220
243
 
221
- if size_changed {
222
- main_panel
223
- .style()
224
- .set_property("width", &format!("{width}px"))?;
225
- main_panel
226
- .style()
227
- .set_property("height", &format!("{height}px"))?;
228
- }
244
+ let (dx, dy) = translate.unwrap_or((0.0, 0.0));
245
+ let moved = dx.abs() > SUBPIXEL_EPSILON || dy.abs() > SUBPIXEL_EPSILON;
246
+ if !size_changed && !moved {
247
+ return Ok(None);
248
+ }
229
249
 
230
- if moved {
231
- main_panel
232
- .style()
233
- .set_property("transform", &format!("translate({dx}px, {dy}px)"))?;
234
- }
250
+ if size_changed {
251
+ main_panel
252
+ .style()
253
+ .set_property("width", &format!("{width}px"))?;
254
+ main_panel
255
+ .style()
256
+ .set_property("height", &format!("{height}px"))?;
257
+ }
235
258
 
236
- if size_changed {
237
- let result = plugin.resize().await;
238
- if translate.is_none() {
239
- main_panel.style().set_property("width", "")?;
240
- main_panel.style().set_property("height", "")?;
241
- }
259
+ if moved {
260
+ main_panel
261
+ .style()
262
+ .set_property("transform", &format!("translate({dx}px, {dy}px)"))?;
263
+ }
242
264
 
243
- result?;
244
- }
265
+ if size_changed {
266
+ let result = plugin.resize().await;
267
+ if translate.is_none() {
268
+ main_panel.style().set_property("width", "")?;
269
+ main_panel.style().set_property("height", "")?;
270
+ }
245
271
 
246
- Ok(None)
247
- })
248
- .await
272
+ result?;
273
+ }
274
+
275
+ Ok(None)
249
276
  }
250
277
 
251
278
  /// Remove the inline presize styles applied by [`Self::presize_with_box`].
@@ -53,6 +53,22 @@ use crate::utils::*;
53
53
  /// presize paths.
54
54
  pub(crate) const SUBPIXEL_EPSILON: f64 = 0.5;
55
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
+
56
72
  /// Everything a plugin may read back during its render, all belonging to
57
73
  /// one snapshot (invariant I5). Built once per bound `View` by the render
58
74
  /// pipeline and CACHED on the `Renderer`; pinning it for a run is an `Rc`
@@ -104,6 +120,20 @@ pub struct RendererData {
104
120
  /// [`Renderer::presize_with_box`] presize calls.
105
121
  presize_pending: Cell<u32>,
106
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
+
107
137
  /// Fires after every draw/update with the computed render limits.
108
138
  pub on_render_limits_changed: RefCell<Option<Callback<RenderLimits>>>,
109
139
 
@@ -221,6 +251,7 @@ impl Deref for RendererData {
221
251
 
222
252
  impl Renderer {
223
253
  pub fn new(viewer_elem: &HtmlElement) -> Self {
254
+ let draw_lock = DebounceMutex::default();
224
255
  Self(Rc::new(RendererData {
225
256
  plugin_data: RefCell::new(RendererMutData {
226
257
  viewer_elem: viewer_elem.clone(),
@@ -231,7 +262,9 @@ impl Renderer {
231
262
  timer: MovingWindowRenderTimer::default(),
232
263
  plugin_states: HashMap::default(),
233
264
  }),
234
- draw_lock: Default::default(),
265
+ geometry_slot: draw_lock.slot(),
266
+ geometry_cmd: Cell::new(None),
267
+ draw_lock,
235
268
  plugin_changed: Default::default(),
236
269
  style_changed: Default::default(),
237
270
  reset_changed: Default::default(),
@@ -52,6 +52,24 @@ pub impl ViewConfigUpdate {
52
52
  self.group_rollup_mode
53
53
  );
54
54
  }
55
+
56
+ let split_rollup_features = metadata
57
+ .get_features()
58
+ .map(|x| x.get_split_rollup_modes())
59
+ .unwrap_or_default();
60
+
61
+ let split_rollups = config_static.get_split_rollups(&split_rollup_features);
62
+ if !split_rollups.contains(
63
+ self.split_rollup_mode
64
+ .as_ref()
65
+ .unwrap_or(&SplitRollupMode::Flat),
66
+ ) {
67
+ self.split_rollup_mode = split_rollups.first().cloned();
68
+ tracing::debug!(
69
+ "Setting plugin-advised split rollup mode {:?}",
70
+ self.split_rollup_mode
71
+ );
72
+ }
55
73
  }
56
74
 
57
75
  /// Appends additional columns to the `columns` field of this
@@ -96,9 +96,27 @@ pub impl ViewConfig {
96
96
  config.filter.retain(|x| x.column() != column);
97
97
  update.filter = Some(config.filter.clone());
98
98
  },
99
+ DragEffect::Move(
100
+ DragTarget::WindowSource
101
+ | DragTarget::WindowOrderBy
102
+ | DragTarget::WindowPartitionBy,
103
+ ) => {
104
+ // Staged origins (window-editor pills) have nothing to remove
105
+ // HERE - the editor's draft is not part of the view config
106
+ // this update mutates. The open `WindowEditor` observes the
107
+ // same `drop_received` event and clears the origin slot in
108
+ // its draft, completing the move.
109
+ },
99
110
  }
100
111
 
101
112
  match drop {
113
+ DragTarget::WindowSource
114
+ | DragTarget::WindowOrderBy
115
+ | DragTarget::WindowPartitionBy => {
116
+ // Unreachable: staged drops never build config updates
117
+ // (`DragTarget::is_staged` is excluded by every committing
118
+ // `drop_received` subscriber).
119
+ },
102
120
  DragTarget::Active => {
103
121
  if !is_swap_to_after_last {
104
122
  if is_to_swap || is_from_required {
@@ -69,6 +69,7 @@ pub struct SessionMetadataState {
69
69
  edit_port: f64,
70
70
  view_schema: Option<HashMap<String, ColumnType>>,
71
71
  expr_meta: Option<SessionViewExpressionMetadata>,
72
+ window_schema: HashMap<String, ColumnType>,
72
73
  }
73
74
 
74
75
  impl SessionMetadata {
@@ -95,6 +96,41 @@ impl SessionMetadata {
95
96
  Ok(())
96
97
  }
97
98
 
99
+ /// Records the pre-aggregation types of the config's window columns, so
100
+ /// `get_column_table_type` resolves them like any other derived column.
101
+ pub(super) fn update_windows(
102
+ &mut self,
103
+ windows: &perspective_client::config::Windows,
104
+ ) -> ApiResult<()> {
105
+ let window_schema = windows
106
+ .iter()
107
+ .filter_map(|(name, w)| {
108
+ let source = self.get_column_table_type(&w.column)?;
109
+ let dtype = self
110
+ .get_window_aggregate(source, &w.aggregate)
111
+ .and_then(|spec| spec.result_type)
112
+ .and_then(|ty| ColumnType::try_from(ty).ok())
113
+ .unwrap_or(source);
114
+
115
+ Some((name.clone(), dtype))
116
+ })
117
+ .collect();
118
+ self.as_mut().unwrap().window_schema = window_schema;
119
+ Ok(())
120
+ }
121
+
122
+ /// The declaration for one window aggregate over a `source` column type.
123
+ pub fn get_window_aggregate(
124
+ &self,
125
+ source: ColumnType,
126
+ name: &str,
127
+ ) -> Option<perspective_client::proto::WindowAggregateArgs> {
128
+ self.get_features()?
129
+ .get_window_aggregates(source)
130
+ .into_iter()
131
+ .find(|x| x.name == name)
132
+ }
133
+
98
134
  /// Whether `view_schema` has been populated by a prior successful
99
135
  /// `create_view`. Used by `Session::create_view` to force a full
100
136
  /// build on first run even when `is_clean` happens to be true, so
@@ -212,6 +248,19 @@ impl SessionMetadata {
212
248
  self.as_ref().map(|meta| &meta.column_names)
213
249
  }
214
250
 
251
+ pub fn is_column_window(&self, name: &str) -> bool {
252
+ self.as_ref()
253
+ .map(|meta| meta.window_schema.contains_key(name))
254
+ .unwrap_or_default()
255
+ }
256
+
257
+ pub fn get_window_columns(&self) -> impl Iterator<Item = &'_ String> {
258
+ self.as_ref()
259
+ .map(|meta| meta.window_schema.keys())
260
+ .into_iter()
261
+ .flatten()
262
+ }
263
+
215
264
  pub fn is_column_expression(&self, name: &str) -> bool {
216
265
  let is_expr: Option<bool> = try {
217
266
  self.as_ref()?
@@ -253,14 +302,18 @@ impl SessionMetadata {
253
302
  pub fn get_column_table_type(&self, name: &str) -> Option<ColumnType> {
254
303
  try {
255
304
  let meta = self.as_ref()?;
256
- meta.table_schema.get(name).cloned().or_else(|| {
257
- meta.expr_meta
258
- .as_ref()?
259
- .expressions
260
- .expression_schema
261
- .get(name)
262
- .cloned()
263
- })?
305
+ meta.table_schema
306
+ .get(name)
307
+ .cloned()
308
+ .or_else(|| {
309
+ meta.expr_meta
310
+ .as_ref()?
311
+ .expressions
312
+ .expression_schema
313
+ .get(name)
314
+ .cloned()
315
+ })
316
+ .or_else(|| meta.window_schema.get(name).cloned())?
264
317
  }
265
318
  }
266
319
 
@@ -282,7 +335,9 @@ impl SessionMetadata {
282
335
  /// expressions.
283
336
  pub fn locator_name_or_default(&self, locator: &ColumnLocator) -> String {
284
337
  match locator {
285
- ColumnLocator::Table(s) | ColumnLocator::Expression(s) => s.clone(),
338
+ ColumnLocator::Table(s) | ColumnLocator::Expression(s) | ColumnLocator::Window(s) => {
339
+ s.clone()
340
+ },
286
341
  ColumnLocator::NewExpression => self.make_new_column_name(None),
287
342
  }
288
343
  }
@@ -126,6 +126,8 @@ pub impl ViewConfig {
126
126
  filter_op: None,
127
127
  group_by_depth: None,
128
128
  group_rollup_mode: None,
129
+ split_rollup_mode: None,
130
+ windows: None,
129
131
  }
130
132
  }
131
133
  }
@@ -806,11 +806,11 @@ impl Session {
806
806
  }
807
807
 
808
808
  /// SYNC name validation for a candidate config (I4): every referenced
809
- /// column must be a table column or an expression present in the
810
- /// candidate itself (syntactic presence — server-side compilability is a
811
- /// run property, not a commit property). Skipped when no table is bound
812
- /// yet: the config rides along until `load()` binds one, and the engine
813
- /// surfaces any residual error on that run.
809
+ /// column must be a table column, or an expression or window column
810
+ /// present in the candidate itself (syntactic presence — server-side
811
+ /// compilability is a run property, not a commit property). Skipped
812
+ /// when no table is bound yet: the config rides along until `load()`
813
+ /// binds one, and the engine surfaces any residual error on that run.
814
814
  fn validate_names(&self, config: &ViewConfig) -> ApiResult<()> {
815
815
  let table_columns = self.all_columns();
816
816
  if table_columns.is_empty() {
@@ -819,6 +819,7 @@ impl Session {
819
819
 
820
820
  let mut allowed: HashSet<&str> = table_columns.iter().map(|x| x.as_str()).collect();
821
821
  allowed.extend(config.expressions.0.keys().map(|x| x.as_str()));
822
+ allowed.extend(config.windows.keys().map(|x| x.as_str()));
822
823
  let named = config
823
824
  .columns
824
825
  .iter()
@@ -1144,6 +1145,7 @@ impl Session {
1144
1145
  let view = table.view(Some(view_config.into())).await?;
1145
1146
  let view_schema = view.schema().await?;
1146
1147
  self.metadata_mut().update_view_schema(&view_schema)?;
1148
+ self.metadata_mut().update_windows(&effective.windows)?;
1147
1149
  let on_stats = Callback::from({
1148
1150
  let this = self.clone();
1149
1151
  move |stats| this.update_stats(stats)
@@ -69,13 +69,23 @@ fn wire_panel_render_sub(session: &Session, renderer: &Renderer) -> Subscription
69
69
  })
70
70
  }
71
71
 
72
- /// Create a new independent panel (own `Session` + `Renderer` + id) from a
73
- /// `ViewerConfigUpdate`, mount and draw it, and return its id. Shared by
74
- /// `addPanel`, whole-element `restore`, and `restorePanel`'s create-if-missing
75
- /// path. `id` is the panel's id — provided when restoring into a specific named
76
- /// slot, or `None` to generate a fresh one. `settings`/`theme` are stripped
77
- /// (element-level, not per-panel) and `client` — or the element's default
78
- /// client when `None` — is bound so the config's `table` resolves against it.
72
+ /// Create a new independent panel (own `Session` + `Renderer` + id) and
73
+ /// restore `update` into it. Shared by `addPanel`, `restore`'s
74
+ /// create-if-missing upsert, and `restorePanel`. `id` is the panel's id —
75
+ /// provided when restoring into a specific named slot, or `None` to
76
+ /// generate a fresh one. `theme` is stripped (element-level, not
77
+ /// per-panel) and `client` — or the element's default client when `None` —
78
+ /// is bound so the config's `table` resolves against it.
79
+ ///
80
+ /// A `table` is OPTIONAL here: an update with none creates a deferred
81
+ /// panel that a later `load()` binds, which is the pre-existing
82
+ /// `restore`-then-`load` contract and the same state the reserved
83
+ /// `load()` path produces. The routes where a table-less panel WOULD be
84
+ /// permanently blank require one in their own argument types instead —
85
+ /// `addPanel`'s [`ViewerConfigInitial`], `WorkspaceConfigUpdate`'s
86
+ /// `panels` entries, and the agent's `add_panel` — so the guarantee sits
87
+ /// at the boundary that owns it rather than here, which would sweep
88
+ /// `restore`'s patch in with them.
79
89
  pub(crate) async fn create_panel(
80
90
  elem: &HtmlElement,
81
91
  presentation: &Presentation,
@@ -128,9 +138,9 @@ pub(crate) async fn create_panel(
128
138
  &renderer,
129
139
  presentation,
130
140
  workspace,
131
- None,
132
141
  RestoreMode::Fresh,
133
142
  update,
143
+ crate::tasks::RestoreErrors::Publish,
134
144
  )
135
145
  .await;
136
146
 
@@ -0,0 +1,155 @@
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 perspective_client::clone;
14
+ use perspective_client::config::{Filter, ViewConfigUpdate, WindowSpec};
15
+
16
+ use super::apply_and_render;
17
+ use crate::presentation::{ColumnLocator, ColumnSettingsTab, OpenColumnSettings, Presentation};
18
+ use crate::renderer::Renderer;
19
+ use crate::session::Session;
20
+ use crate::*;
21
+
22
+ fn reopen(presentation: &Presentation, name: String) {
23
+ presentation.set_open_column_settings(Some(OpenColumnSettings {
24
+ locator: Some(ColumnLocator::Window(name)),
25
+ tab: Some(ColumnSettingsTab::Window),
26
+ }));
27
+ }
28
+
29
+ /// Insert `spec` into the session's windows as `name` (replacing any spec
30
+ /// under the same key) and re-render. After the render completes, opens the
31
+ /// column-settings drawer on the new window column.
32
+ pub fn save_window(
33
+ session: &Session,
34
+ renderer: &Renderer,
35
+ presentation: &Presentation,
36
+ name: String,
37
+ spec: WindowSpec,
38
+ ) -> ApiResult<()> {
39
+ let presentation = presentation.clone();
40
+ let task = {
41
+ let mut windows = session.get_view_config().windows.clone();
42
+ windows.insert(name.clone(), spec);
43
+ apply_and_render(session, renderer, ViewConfigUpdate {
44
+ windows: Some(windows),
45
+ ..Default::default()
46
+ })
47
+ }?;
48
+
49
+ ApiFuture::spawn(async move {
50
+ task.await?;
51
+ reopen(&presentation, name);
52
+ Ok(())
53
+ });
54
+
55
+ Ok(())
56
+ }
57
+
58
+ /// Replace the window at `old_name` with `spec` and re-render. A rename
59
+ /// rewrites every config reference to the old name - `columns`, `group_by`,
60
+ /// `split_by`, `sort`, `filter` and the `aggregates` key - so usage follows
61
+ /// the column, as expression renames do.
62
+ pub fn update_window(
63
+ session: &Session,
64
+ renderer: &Renderer,
65
+ presentation: &Presentation,
66
+ old_name: String,
67
+ new_name: String,
68
+ spec: WindowSpec,
69
+ ) {
70
+ clone!(session, renderer, presentation);
71
+ ApiFuture::spawn(async move {
72
+ let update = {
73
+ let config = session.get_view_config().clone();
74
+ let mut windows = config.windows.clone();
75
+ windows.remove(&old_name);
76
+ windows.insert(new_name.clone(), spec);
77
+
78
+ let rename = |col: &String| -> String {
79
+ if col == &old_name {
80
+ new_name.clone()
81
+ } else {
82
+ col.clone()
83
+ }
84
+ };
85
+
86
+ let mut update = ViewConfigUpdate {
87
+ windows: Some(windows),
88
+ ..Default::default()
89
+ };
90
+
91
+ if old_name != new_name {
92
+ update.columns = Some(
93
+ config
94
+ .columns
95
+ .iter()
96
+ .map(|col| col.as_ref().map(rename))
97
+ .collect(),
98
+ );
99
+ update.group_by = Some(config.group_by.iter().map(&rename).collect());
100
+ update.split_by = Some(config.split_by.iter().map(&rename).collect());
101
+ update.sort = Some(
102
+ config
103
+ .sort
104
+ .iter()
105
+ .map(|sort| {
106
+ let mut sort = sort.clone();
107
+ sort.0 = rename(&sort.0);
108
+ sort
109
+ })
110
+ .collect(),
111
+ );
112
+ update.filter = Some(
113
+ config
114
+ .filter
115
+ .iter()
116
+ .map(|filter| {
117
+ if filter.column() == old_name {
118
+ Filter::new(new_name.as_str(), filter.op(), filter.term().clone())
119
+ } else {
120
+ filter.clone()
121
+ }
122
+ })
123
+ .collect(),
124
+ );
125
+ update.aggregates = Some(
126
+ config
127
+ .aggregates
128
+ .iter()
129
+ .map(|(col, agg)| (rename(col), agg.clone()))
130
+ .collect(),
131
+ );
132
+ }
133
+
134
+ update
135
+ };
136
+
137
+ apply_and_render(&session, &renderer, update)?.await?;
138
+ reopen(&presentation, new_name);
139
+ Ok(())
140
+ });
141
+ }
142
+
143
+ /// Remove the window named `name` from the session and re-render.
144
+ pub fn delete_window(session: &Session, renderer: &Renderer, name: &str) -> ApiResult<()> {
145
+ let mut windows = session.get_view_config().windows.clone();
146
+ windows.remove(name);
147
+ let config = ViewConfigUpdate {
148
+ windows: Some(windows),
149
+ ..ViewConfigUpdate::default()
150
+ };
151
+
152
+ let task = apply_and_render(session, renderer, config)?;
153
+ ApiFuture::spawn(task);
154
+ Ok(())
155
+ }
@@ -27,6 +27,7 @@ mod copy_export;
27
27
  mod create_panel;
28
28
  mod dismiss_render_warning;
29
29
  mod edit_expression;
30
+ mod edit_window;
30
31
  mod eject;
31
32
  mod pipeline;
32
33
  mod presize_panels;
@@ -58,6 +59,7 @@ pub use self::copy_export::*;
58
59
  pub(crate) use self::create_panel::*;
59
60
  pub use self::dismiss_render_warning::*;
60
61
  pub use self::edit_expression::*;
62
+ pub use self::edit_window::*;
61
63
  pub use self::eject::*;
62
64
  pub use self::pipeline::*;
63
65
  pub use self::presize_panels::*;