@perspective-dev/viewer 5.2.0 → 5.3.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 (133) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  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/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/docs/perspective-docs.json +1 -1
  17. package/dist/esm/perspective-viewer.d.ts +1 -0
  18. package/dist/esm/perspective-viewer.inline.js +9 -9
  19. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  20. package/dist/esm/perspective-viewer.js +4 -4
  21. package/dist/esm/perspective-viewer.js.map +2 -2
  22. package/dist/esm/plugin.d.ts +10 -0
  23. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
  24. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  25. package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
  26. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  27. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
  28. package/dist/wasm/perspective-viewer.d.ts +43 -36
  29. package/dist/wasm/perspective-viewer.js +194 -136
  30. package/dist/wasm/perspective-viewer.wasm +0 -0
  31. package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
  32. package/package.json +1 -1
  33. package/src/css/column-settings-panel.css +9 -5
  34. package/src/css/column-style.css +209 -31
  35. package/src/css/dom/select.css +2 -1
  36. package/src/css/form/code-editor.css +4 -4
  37. package/src/rust/agent/tools.rs +48 -17
  38. package/src/rust/components/column_selector/active_column.rs +2 -2
  39. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  40. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  41. package/src/rust/components/column_selector/config_selector.rs +8 -0
  42. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  43. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  44. package/src/rust/components/column_selector.rs +48 -4
  45. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  46. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  47. package/src/rust/components/column_settings_sidebar.rs +88 -69
  48. package/src/rust/components/editable_header.rs +85 -144
  49. package/src/rust/components/expression_editor.rs +9 -16
  50. package/src/rust/components/form/code_editor.rs +16 -6
  51. package/src/rust/components/form/debug.rs +1 -0
  52. package/src/rust/components/form/mod.rs +3 -1
  53. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  54. package/src/rust/components/form/named_value_picker.rs +86 -0
  55. package/src/rust/components/form/palette_selector.rs +440 -0
  56. package/src/rust/components/main_panel/msg.rs +2 -2
  57. package/src/rust/components/main_panel/reconcile.rs +4 -4
  58. package/src/rust/components/plugin_tab.rs +4 -28
  59. package/src/rust/components/settings_panel.rs +25 -20
  60. package/src/rust/components/viewer/msg.rs +46 -7
  61. package/src/rust/components/viewer/panels.rs +11 -9
  62. package/src/rust/components/viewer/render.rs +8 -3
  63. package/src/rust/components/viewer/settings.rs +163 -23
  64. package/src/rust/components/viewer/snapshots.rs +46 -21
  65. package/src/rust/components/viewer/wiring.rs +14 -25
  66. package/src/rust/components/viewer.rs +12 -8
  67. package/src/rust/config/column_config_schema.rs +100 -41
  68. package/src/rust/config/css_scale.rs +882 -0
  69. package/src/rust/config/mod.rs +2 -0
  70. package/src/rust/config/options.rs +10 -0
  71. package/src/rust/config/viewer_config.rs +49 -55
  72. package/src/rust/config/workspace_config.rs +132 -5
  73. package/src/rust/custom_elements/viewer.rs +214 -155
  74. package/src/rust/custom_events.rs +14 -1
  75. package/src/rust/js/regular_layout.rs +3 -3
  76. package/src/rust/lib.rs +2 -0
  77. package/src/rust/presentation/column_locator.rs +19 -5
  78. package/src/rust/presentation.rs +192 -36
  79. package/src/rust/queries/column_locator.rs +74 -44
  80. package/src/rust/queries/get_viewer_config.rs +0 -3
  81. package/src/rust/queries/mod.rs +2 -0
  82. package/src/rust/queries/palette_set.rs +82 -0
  83. package/src/rust/queries/plugin_column_styles.rs +6 -2
  84. package/src/rust/renderer/dispatch.rs +16 -4
  85. package/src/rust/renderer/plugin_config.rs +309 -47
  86. package/src/rust/renderer.rs +15 -42
  87. package/src/rust/session/column_defaults_update.rs +15 -8
  88. package/src/rust/session/metadata.rs +5 -1
  89. package/src/rust/session/props.rs +1 -0
  90. package/src/rust/session.rs +9 -6
  91. package/src/rust/tasks/create_panel.rs +44 -57
  92. package/src/rust/tasks/edit_expression.rs +19 -23
  93. package/src/rust/tasks/edit_window.rs +8 -10
  94. package/src/rust/tasks/eject.rs +4 -1
  95. package/src/rust/tasks/pipeline.rs +2 -2
  96. package/src/rust/tasks/presize_panels.rs +44 -5
  97. package/src/rust/tasks/reset_all.rs +4 -4
  98. package/src/rust/tasks/restore_and_render.rs +17 -44
  99. package/src/rust/tasks/restore_panel.rs +11 -13
  100. package/src/rust/tasks/set_edit_mode.rs +3 -1
  101. package/src/rust/tasks/sync_update_panels.rs +11 -1
  102. package/src/rust/tasks/update_theme.rs +23 -18
  103. package/src/rust/utils/css_vars.rs +26 -0
  104. package/src/rust/utils/mod.rs +2 -0
  105. package/src/rust/utils/spawn.rs +1 -1
  106. package/src/rust/workspace.rs +435 -272
  107. package/src/svg/lock.svg +7 -0
  108. package/src/themes/botanical.css +10 -0
  109. package/src/themes/defaults.css +12 -0
  110. package/src/themes/dracula.css +17 -0
  111. package/src/themes/gruvbox-dark.css +20 -0
  112. package/src/themes/gruvbox.css +10 -0
  113. package/src/themes/icons.css +1 -0
  114. package/src/themes/monokai.css +22 -0
  115. package/src/themes/phosphor.css +5 -0
  116. package/src/themes/pro-dark.css +10 -0
  117. package/src/themes/solarized-dark.css +7 -0
  118. package/src/themes/solarized.css +7 -0
  119. package/src/themes/vaporwave.css +20 -0
  120. package/src/ts/perspective-viewer.ts +1 -0
  121. package/src/ts/plugin.ts +14 -0
  122. package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
  123. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  124. package/src/ts/ts-rs/ViewerConfig.ts +1 -1
  125. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  126. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
  127. package/src/rust/components/form/color_range_selector.rs +0 -137
  128. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  129. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  130. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  131. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  132. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  133. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -66,6 +66,23 @@ impl ColumnLocator {
66
66
  }
67
67
  }
68
68
 
69
+ /// What the column-settings drawer is opened on, as held in presentation
70
+ /// state: a column by name, or the not-yet-saved expression.
71
+ #[derive(Clone, Debug, PartialEq)]
72
+ pub enum ColumnSettingsTarget {
73
+ Column(String),
74
+ NewExpression,
75
+ }
76
+
77
+ impl ColumnSettingsTarget {
78
+ pub fn name(&self) -> Option<&String> {
79
+ match self {
80
+ Self::Column(name) => Some(name),
81
+ Self::NewExpression => None,
82
+ }
83
+ }
84
+ }
85
+
69
86
  #[derive(Debug, Default, Clone, Copy, PartialEq)]
70
87
 
71
88
  pub enum ColumnSettingsTab {
@@ -83,16 +100,13 @@ impl Display for ColumnSettingsTab {
83
100
 
84
101
  #[derive(Clone, Default, Debug, PartialEq)]
85
102
  pub struct OpenColumnSettings {
86
- pub locator: Option<ColumnLocator>,
103
+ pub target: Option<ColumnSettingsTarget>,
87
104
  pub tab: Option<ColumnSettingsTab>,
88
105
  }
89
106
 
90
107
  impl OpenColumnSettings {
91
108
  pub fn name(&self) -> Option<String> {
92
- self.locator
93
- .as_ref()
94
- .and_then(|l| l.name())
95
- .map(|s| s.to_owned())
109
+ self.target.as_ref().and_then(|t| t.name()).cloned()
96
110
  }
97
111
  }
98
112
 
@@ -15,8 +15,8 @@ pub mod drag_helpers;
15
15
  mod props;
16
16
  mod sheets;
17
17
 
18
- use std::cell::RefCell;
19
- use std::collections::HashSet;
18
+ use std::cell::{Cell, RefCell};
19
+ use std::collections::{BTreeMap, HashSet};
20
20
  use std::ops::Deref;
21
21
  use std::rc::Rc;
22
22
 
@@ -27,10 +27,13 @@ use web_sys::*;
27
27
  use yew::html::ImplicitClone;
28
28
  use yew::prelude::*;
29
29
 
30
- pub use self::column_locator::{ColumnLocator, ColumnSettingsTab, ColumnTab, OpenColumnSettings};
30
+ pub use self::column_locator::{
31
+ ColumnLocator, ColumnSettingsTab, ColumnSettingsTarget, ColumnTab, OpenColumnSettings,
32
+ };
31
33
  use self::drag_helpers::DragTargetState;
32
34
  pub use self::drag_helpers::{DragDropContainer, DragEndCallback};
33
35
  pub use self::props::{DragDropProps, PresentationProps};
36
+ use crate::config::{CssKind, NamedValue, assign_palette_names};
34
37
  use crate::utils::*;
35
38
 
36
39
  #[derive(Clone, Debug)]
@@ -85,6 +88,18 @@ pub struct PresentationHandle {
85
88
  /// concurrent `get_available_themes` calls await one parse instead of
86
89
  /// racing their own.
87
90
  theme_init: Mutex<()>,
91
+
92
+ /// Whether the host's theme was ever EXPLICITLY chosen — authored as a
93
+ /// `theme` attribute, or set by name through [`Self::set_theme_name`].
94
+ ///
95
+ /// `false` means the host merely displays the registry default, which
96
+ /// [`Self::reset_themes`] is free to move; `true` pins the selection
97
+ /// until it leaves the registry. The `theme` attribute alone cannot
98
+ /// carry this, because `init` stamps it unconditionally so the document
99
+ /// cascade has a theme to match.
100
+ theme_selected: Cell<bool>,
101
+
102
+ palette: RefCell<BTreeMap<String, String>>,
88
103
  is_settings_open: RefCell<bool>,
89
104
  open_column_settings: RefCell<OpenColumnSettings>,
90
105
  is_workspace: RefCell<Option<bool>>,
@@ -109,11 +124,20 @@ pub struct PresentationHandle {
109
124
  /// dragged element from the shadow tree.
110
125
  host_dragend: RefCell<Option<DragEndCallback>>,
111
126
 
127
+ source_dragend: RefCell<Option<(web_sys::EventTarget, DragEndCallback)>>,
128
+
112
129
  /// IntersectionObserver-based fallback for the drag image, kept alive for
113
130
  /// the duration of the drag.
114
131
  drag_target: RefCell<Option<DragTargetState>>,
115
132
 
116
- pub settings_open_changed: PubSub<bool>,
133
+ /// `(open, announce)` — `announce` says this toggle is the SOLE carrier
134
+ /// of the config change (a user gesture: toolbar, `toggleConfig`), so
135
+ /// the element must emit `toggle-settings` + a config-update for it. An
136
+ /// API restore's toggle is announced by its own view-config commit
137
+ /// dispatch instead — announcing here too would double-emit, the first
138
+ /// carrying the intermediate config (settings flipped, view fields not
139
+ /// yet committed).
140
+ pub settings_open_changed: PubSub<(bool, bool)>,
117
141
 
118
142
  /// Injected callback from the root component, replacing the former
119
143
  /// `is_workspace_changed: PubSub` field.
@@ -158,6 +182,8 @@ impl Presentation {
158
182
  agent: Default::default(),
159
183
  themes: Default::default(),
160
184
  theme_init: Default::default(),
185
+ theme_selected: Cell::new(elem.get_attribute("theme").is_some()),
186
+ palette: Default::default(),
161
187
  is_workspace: Default::default(),
162
188
  settings_open_changed: Default::default(),
163
189
  settings_before_open_changed: Default::default(),
@@ -173,6 +199,7 @@ impl Presentation {
173
199
  on_dragstart: Default::default(),
174
200
  on_dragend: Default::default(),
175
201
  host_dragend: Default::default(),
202
+ source_dragend: Default::default(),
176
203
  drag_target: Default::default(),
177
204
  }));
178
205
 
@@ -238,13 +265,84 @@ impl Presentation {
238
265
  }
239
266
  }
240
267
 
241
- pub fn set_settings_open(&self, open: bool) {
242
- self.settings_open_changed.emit(open);
268
+ /// See [`PresentationHandle::settings_open_changed`] for `announce`.
269
+ pub fn set_settings_open(&self, open: bool, announce: bool) {
270
+ self.settings_open_changed.emit((open, announce));
243
271
  }
244
272
 
245
273
  /// Sets the currently opened column settings. Emits an internal event on
246
274
  /// change. Passing None is a shorthand for setting all fields to
247
275
  /// None.
276
+ /// The workspace palette as last restored (canonical values).
277
+ pub fn palette(&self) -> BTreeMap<String, String> {
278
+ self.0.palette.borrow().clone()
279
+ }
280
+
281
+ /// Replace the host palette: every previously-applied `--psp-user--*`
282
+ /// inline property is removed, then `palette` is applied.
283
+ pub fn set_palette(&self, palette: BTreeMap<String, String>) -> ApiResult<()> {
284
+ let style = self.0.viewer_elem.style();
285
+ for name in self.0.palette.borrow().keys() {
286
+ style.remove_property(name)?;
287
+ }
288
+
289
+ for (name, value) in &palette {
290
+ style.set_property(name, value)?;
291
+ }
292
+
293
+ *self.0.palette.borrow_mut() = palette;
294
+ Ok(())
295
+ }
296
+
297
+ /// Pin `literal` into the restored palette, named by the same rules
298
+ /// as the derived set and applied to the host.
299
+ pub fn pin_style(&self, kind: CssKind, literal: &str) -> ApiResult<()> {
300
+ let Ok(value) = kind.canonicalize(literal) else {
301
+ return Ok(());
302
+ };
303
+
304
+ let current = self.palette();
305
+ let host = self.host_named_values(kind);
306
+ let set = assign_palette_names(&current, &host, &[(kind, value)], &|name| {
307
+ self.resolve_css_var(name).is_some()
308
+ });
309
+
310
+ let style = self.0.viewer_elem.style();
311
+ for (name, value) in &set {
312
+ if !current.contains_key(name) {
313
+ style.set_property(name, value)?;
314
+ }
315
+ }
316
+
317
+ *self.0.palette.borrow_mut() = set;
318
+ Ok(())
319
+ }
320
+
321
+ /// The host's computed value for custom property `name` (inline
322
+ /// palette, then theme/page stylesheets), or `None` if undefined.
323
+ pub fn resolve_css_var(&self, name: &str) -> Option<String> {
324
+ read_custom_property(&self.0.viewer_elem, name)
325
+ }
326
+
327
+ /// Theme/page-authored named values of `kind` on the host, canonical,
328
+ /// discovered by the contiguous-numbering walk `--psp-user--<kind>-1`,
329
+ /// `-2`, … up to the first undefined name.
330
+ pub fn host_named_values(&self, kind: CssKind) -> Vec<NamedValue> {
331
+ let mut out = vec![];
332
+ for n in 1.. {
333
+ let name = format!("{}{n}", kind.var_prefix());
334
+ let Some(raw) = self.resolve_css_var(&name) else {
335
+ break;
336
+ };
337
+
338
+ if let Ok(value) = kind.canonicalize(&raw) {
339
+ out.push(NamedValue { name, value });
340
+ }
341
+ }
342
+
343
+ out
344
+ }
345
+
248
346
  pub fn set_open_column_settings(&self, settings: Option<OpenColumnSettings>) {
249
347
  let settings = settings.unwrap_or_default();
250
348
  if *(self.open_column_settings.borrow()) != settings {
@@ -315,22 +413,24 @@ impl Presentation {
315
413
  index.and_then(|x| themes.get(x).cloned())
316
414
  }
317
415
 
318
- /// The default theme (first registered), used to resolve a panel's
319
- /// effective theme when it has no per-panel override. `None` if no
416
+ /// The theme a NEW panel is born with: the host's if it has one, else
417
+ /// the registry default. Synchronous, because panel creation is — the
418
+ /// registry fallback is `None` until the registry first parses, which
419
+ /// [`crate::tasks::seed_panel_theme`] fills in.
420
+ pub fn active_theme_name_sync(&self) -> Option<String> {
421
+ self.0
422
+ .viewer_elem
423
+ .get_attribute("theme")
424
+ .or_else(|| self.0.themes.borrow().as_ref()?.first().cloned())
425
+ }
426
+
427
+ /// The registry default — the FIRST registered theme, which a panel or
428
+ /// host resolves to only when it has no theme of its own. `None` if no
320
429
  /// themes exist.
321
430
  pub async fn get_default_theme_name(&self) -> Option<String> {
322
431
  self.get_available_themes().await.ok()?.first().cloned()
323
432
  }
324
433
 
325
- /// SYNC read of the registry default theme name, derived on demand from
326
- /// the memoized registry — `None` until the registry first parses (or
327
- /// when no themes exist). For synchronous stamping paths that must not
328
- /// await registry init; prefer [`Self::get_default_theme_name`] where
329
- /// awaiting is acceptable.
330
- pub fn default_theme_name_sync(&self) -> Option<String> {
331
- self.0.themes.borrow().as_ref()?.first().cloned()
332
- }
333
-
334
434
  fn set_theme_attribute(&self, theme: Option<&str>) -> ApiResult<()> {
335
435
  if let Some(theme) = theme {
336
436
  Ok(self.0.viewer_elem.set_attribute("theme", theme)?)
@@ -341,12 +441,39 @@ impl Presentation {
341
441
 
342
442
  pub async fn reset_theme(&self) -> ApiResult<()> {
343
443
  *self.0.is_workspace.borrow_mut() = None;
344
- let themes = self.get_available_themes().await?;
345
- let default_theme = themes.first().map(|x| x.as_str());
346
- self.set_theme_name(default_theme).await?;
444
+ self.set_theme_name(None).await?;
347
445
  Ok(())
348
446
  }
349
447
 
448
+ /// Adopt `themes` as the available set, KEEPING the host's theme unless
449
+ /// it was never explicitly chosen or has left the set — the only two
450
+ /// cases in which re-ordering the registry may move the viewer.
451
+ ///
452
+ /// Always re-stamps; the caller publishes ([`Self::publish_theme_config`])
453
+ /// once its restyles have resolved, because the available list has
454
+ /// changed even when the selection has not.
455
+ ///
456
+ /// @param themes the new set, or `None` to re-parse the document.
457
+ ///
458
+ /// # Returns
459
+ /// The active theme after the change.
460
+ pub async fn reset_themes(&self, themes: Option<Vec<String>>) -> ApiResult<Option<String>> {
461
+ let selected = self
462
+ .0
463
+ .theme_selected
464
+ .get()
465
+ .then(|| self.0.viewer_elem.get_attribute("theme"))
466
+ .flatten();
467
+
468
+ self.reset_available_themes(themes).await;
469
+ let available = self.get_available_themes().await?;
470
+ let kept = selected.filter(|name| available.contains(name));
471
+ self.0.theme_selected.set(kept.is_some());
472
+ let active = kept.or_else(|| available.first().cloned());
473
+ self.set_theme_attribute(active.as_deref())?;
474
+ Ok(active)
475
+ }
476
+
350
477
  /// Set the theme by name, or `None` for the default theme.
351
478
  ///
352
479
  /// A NAMED theme's host attribute write is SYNCHRONOUS ("stamp with
@@ -368,6 +495,7 @@ impl Presentation {
368
495
  /// # Returns
369
496
  /// A `bool` indicating whether the internal state changed.
370
497
  pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult<bool> {
498
+ self.0.theme_selected.set(theme.is_some());
371
499
  if let Some(theme) = theme {
372
500
  if self.0.viewer_elem.get_attribute("theme").as_deref() == Some(theme) {
373
501
  return Ok(false);
@@ -377,20 +505,27 @@ impl Presentation {
377
505
  }
378
506
 
379
507
  let themes = self.get_available_themes().await?;
380
- let index = if let Some(theme) = theme {
381
- themes.iter().position(|x| x == theme)
382
- } else if !themes.is_empty() {
508
+ if theme.is_none() {
383
509
  self.set_theme_attribute(themes.first().map(|x| x.as_str()))?;
384
- Some(0)
385
- } else {
386
- self.set_theme_attribute(None)?;
387
- None
388
- };
510
+ }
389
511
 
390
- self.theme_config_updated.emit((themes, index));
391
512
  Ok(true)
392
513
  }
393
514
 
515
+ /// Publish the `theme_config_updated` snapshot — the available themes
516
+ /// and the host's current selection — to the component tree.
517
+ pub async fn publish_theme_config(&self) -> ApiResult<()> {
518
+ let themes = self.get_available_themes().await?;
519
+ let index = self
520
+ .0
521
+ .viewer_elem
522
+ .get_attribute("theme")
523
+ .and_then(|active| themes.iter().position(|x| *x == active));
524
+
525
+ self.theme_config_updated.emit((themes, index));
526
+ Ok(())
527
+ }
528
+
394
529
  /// Snapshot the drag state as a [`DragDropProps`] value for threading
395
530
  /// through the component tree without PubSub subscriptions.
396
531
  pub fn drag_drop_props(&self) -> DragDropProps {
@@ -427,6 +562,7 @@ impl Presentation {
427
562
 
428
563
  pub fn set_drag_image(&self, event: &DragEvent) -> ApiResult<()> {
429
564
  event.stop_propagation();
565
+ self.register_source_dragend(event)?;
430
566
  if let Some(dt) = event.data_transfer() {
431
567
  dt.set_drop_effect("move");
432
568
  }
@@ -485,8 +621,7 @@ impl Presentation {
485
621
  _ => None,
486
622
  };
487
623
 
488
- self.drag_target.borrow_mut().take();
489
- *self.drag_state.borrow_mut() = DragState::NoDrag;
624
+ self.end_drag();
490
625
  if let Some(action) = action {
491
626
  self.drop_received.emit(action);
492
627
  }
@@ -510,11 +645,15 @@ impl Presentation {
510
645
  /// End the drag/drop action by resetting the state to default.
511
646
  pub fn notify_drag_end(&self) {
512
647
  if self.drag_state.borrow().is_drag_in_progress() {
513
- self.drag_target.borrow_mut().take();
514
- *self.drag_state.borrow_mut() = DragState::NoDrag;
515
- if let Some(cb) = self.on_dragend.borrow().as_ref() {
516
- cb.emit(());
517
- }
648
+ self.end_drag();
649
+ }
650
+ }
651
+
652
+ fn end_drag(&self) {
653
+ self.drag_target.borrow_mut().take();
654
+ *self.drag_state.borrow_mut() = DragState::NoDrag;
655
+ if let Some(cb) = self.on_dragend.borrow().as_ref() {
656
+ cb.emit(());
518
657
  }
519
658
  }
520
659
 
@@ -522,6 +661,23 @@ impl Presentation {
522
661
  /// element so that drag-end cleanup fires even when Yew re-renders
523
662
  /// remove the original dragged element from the shadow DOM. The host
524
663
  /// element is outside the virtual DOM and therefore stable.
664
+ fn register_source_dragend(&self, event: &DragEvent) -> ApiResult<()> {
665
+ let target = event.target().into_apierror()?;
666
+ if let Some((prev_target, prev)) = self.source_dragend.borrow_mut().take() {
667
+ let _ = prev_target
668
+ .remove_event_listener_with_callback("dragend", prev.as_ref().unchecked_ref());
669
+ }
670
+
671
+ let this = self.clone();
672
+ let closure = Closure::wrap(Box::new(move |_event: DragEvent| {
673
+ this.notify_drag_end();
674
+ }) as Box<dyn FnMut(DragEvent)>);
675
+
676
+ target.add_event_listener_with_callback("dragend", closure.as_ref().unchecked_ref())?;
677
+ *self.source_dragend.borrow_mut() = Some((target, closure));
678
+ Ok(())
679
+ }
680
+
525
681
  fn register_host_dragend(&self) {
526
682
  if let Some(prev) = self.host_dragend.borrow_mut().take() {
527
683
  let _ = self
@@ -12,26 +12,29 @@
12
12
 
13
13
  use perspective_client::config::ViewConfig;
14
14
 
15
- use crate::presentation::{ColumnLocator, OpenColumnSettings};
15
+ use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings};
16
16
  use crate::renderer::Renderer;
17
17
  use crate::session::SessionMetadata;
18
18
 
19
- /// Returns a [`ColumnLocator`] for a given column name, or [`None`] if no
20
- /// such column exists.
21
- pub fn get_column_locator(
19
+ /// Classify a column name against the session, config-first: the view
20
+ /// config is the commit of record for expressions and windows.
21
+ pub fn classify_column(
22
+ name: &str,
23
+ view_config: &ViewConfig,
22
24
  metadata: &SessionMetadata,
23
- name: Option<String>,
24
25
  ) -> Option<ColumnLocator> {
25
- name.and_then(|name| {
26
- if metadata.is_column_expression(&name) {
27
- Some(ColumnLocator::Expression(name))
28
- } else {
29
- metadata.get_table_columns().and_then(|x| {
30
- x.iter()
31
- .find_map(|n| (n == &name).then_some(ColumnLocator::Table(name.clone())))
32
- })
33
- }
34
- })
26
+ if view_config.windows.contains_key(name) || metadata.is_column_window(name) {
27
+ Some(ColumnLocator::Window(name.to_owned()))
28
+ } else if view_config.expressions.contains_key(name) || metadata.is_column_expression(name) {
29
+ Some(ColumnLocator::Expression(name.to_owned()))
30
+ } else if metadata
31
+ .get_table_columns()
32
+ .is_some_and(|cols| cols.iter().any(|col| col == name))
33
+ {
34
+ Some(ColumnLocator::Table(name.to_owned()))
35
+ } else {
36
+ None
37
+ }
35
38
  }
36
39
 
37
40
  /// Gets a [`ColumnLocator`] for the current UI's column settings state,
@@ -43,35 +46,62 @@ pub fn get_current_column_locator(
43
46
  open_column_settings: &OpenColumnSettings,
44
47
  renderer: &Renderer,
45
48
  view_config: &ViewConfig,
46
- _metadata: &SessionMetadata,
49
+ metadata: &SessionMetadata,
47
50
  ) -> Option<ColumnLocator> {
48
- open_column_settings
49
- .locator
50
- .clone()
51
- .filter(|locator| match locator {
52
- ColumnLocator::Table(_name) => {
53
- locator
54
- .name()
55
- .map(|name| {
56
- view_config.columns.iter().any(|maybe_col| {
57
- maybe_col
58
- .as_ref()
59
- .map(|col| col == name)
60
- .unwrap_or_default()
51
+ match open_column_settings.target.as_ref()? {
52
+ ColumnSettingsTarget::NewExpression => Some(ColumnLocator::NewExpression),
53
+ ColumnSettingsTarget::Column(name) => {
54
+ let locator = classify_column(name, view_config, metadata)?;
55
+ match locator {
56
+ ColumnLocator::Table(_) => {
57
+ let in_columns = view_config.columns.iter().any(|maybe_col| {
58
+ maybe_col
59
+ .as_ref()
60
+ .map(|col| col == name)
61
+ .unwrap_or_default()
62
+ });
63
+
64
+ (in_columns && renderer.can_render_column_styles()).then_some(locator)
65
+ },
66
+ locator => Some(locator),
67
+ }
68
+ },
69
+ }
70
+ }
71
+
72
+ #[cfg(test)]
73
+ mod tests {
74
+ use std::collections::HashMap;
75
+
76
+ use perspective_client::config::Expressions;
77
+
78
+ use super::*;
79
+
80
+ fn config_with_expression(name: &str, expr: &str) -> ViewConfig {
81
+ ViewConfig {
82
+ expressions: Expressions(HashMap::from([(name.to_owned(), expr.to_owned())])),
83
+ ..ViewConfig::default()
84
+ }
85
+ }
86
+
87
+ #[test]
88
+ fn classifies_from_the_config_before_metadata_catches_up() {
89
+ let config = config_with_expression("x", "1 + 1");
90
+ let metadata = SessionMetadata::default();
91
+ assert_eq!(
92
+ classify_column("x", &config, &metadata),
93
+ Some(ColumnLocator::Expression("x".to_owned()))
94
+ );
95
+ assert_eq!(classify_column("nope", &config, &metadata), None);
96
+ }
61
97
 
62
- // }) || view_config.group_by.iter().any(|col|
63
- // col == name) ||
64
- // view_config.split_by.iter().any(|col| col ==
65
- // name) ||
66
- // view_config.filter.iter().any(|col| col.column()
67
- // == name) ||
68
- // view_config.sort.iter().any(|col| &col.0 == name)
69
- // })
70
- })
71
- })
72
- .unwrap_or_default()
73
- && renderer.can_render_column_styles()
74
- },
75
- _ => true,
76
- })
98
+ #[test]
99
+ fn unnamed_expressions_are_keyed_by_their_text() {
100
+ let config = config_with_expression("\"Sales\" + 1", "\"Sales\" + 1");
101
+ let metadata = SessionMetadata::default();
102
+ assert_eq!(
103
+ classify_column("\"Sales\" + 1", &config, &metadata),
104
+ Some(ColumnLocator::Expression("\"Sales\" + 1".to_owned()))
105
+ );
106
+ }
77
107
  }
@@ -51,9 +51,6 @@ pub async fn get_viewer_config(
51
51
  None => presentation.get_selected_theme_name().await,
52
52
  };
53
53
  let title = session.get_title();
54
- // Placed ⇒ bound (or binding in flight): creation requires a `table`
55
- // by type, so a panel with neither a bound nor pending table is an
56
- // invariant violation, not a serializable state.
57
54
  let table = session
58
55
  .get_table()
59
56
  .map(|x| x.get_name().to_owned())
@@ -29,6 +29,7 @@ mod exports;
29
29
  mod fetch_column_stats;
30
30
  mod get_viewer_config;
31
31
  mod is_invalid_drop;
32
+ mod palette_set;
32
33
  mod plugin_column_styles;
33
34
  mod validate_expression;
34
35
 
@@ -40,5 +41,6 @@ pub use self::exports::*;
40
41
  pub use self::fetch_column_stats::*;
41
42
  pub use self::get_viewer_config::*;
42
43
  pub use self::is_invalid_drop::*;
44
+ pub use self::palette_set::*;
43
45
  pub use self::plugin_column_styles::*;
44
46
  pub use self::validate_expression::*;
@@ -0,0 +1,82 @@
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::collections::BTreeMap;
14
+
15
+ use crate::config::{CssKind, CssLiteralUse, NamedValue, assign_palette_names};
16
+ use crate::presentation::Presentation;
17
+ use crate::workspace::{PanelId, Workspace};
18
+
19
+ /// Every CSS literal in use across the workspace, as `(panel id, use)`,
20
+ /// in a deterministic order.
21
+ pub fn styles_in_use(workspace: &Workspace) -> Vec<(PanelId, CssLiteralUse)> {
22
+ let mut ids = workspace.panel_ids();
23
+ ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
24
+ let mut out = vec![];
25
+ for id in ids {
26
+ let Some(panel) = workspace.panel(&id) else {
27
+ continue;
28
+ };
29
+
30
+ let uses = {
31
+ let view_config = panel.session.get_view_config();
32
+ panel
33
+ .renderer
34
+ .css_literals_in_use(&view_config, &panel.session)
35
+ };
36
+
37
+ out.extend(uses.into_iter().map(|x| (id.clone(), x)));
38
+ }
39
+
40
+ out
41
+ }
42
+
43
+ /// The workspace palette set: the restored palette ∪ every literal in
44
+ /// use, named stably (see [`crate::config::assign_palette_names`]).
45
+ pub fn palette_set(workspace: &Workspace, presentation: &Presentation) -> BTreeMap<String, String> {
46
+ let host: Vec<NamedValue> = CssKind::ALL
47
+ .into_iter()
48
+ .flat_map(|kind| presentation.host_named_values(kind))
49
+ .collect();
50
+
51
+ let in_use: Vec<(CssKind, String)> = styles_in_use(workspace)
52
+ .into_iter()
53
+ .map(|(_, x)| (x.kind, x.literal))
54
+ .collect();
55
+
56
+ assign_palette_names(&presentation.palette(), &host, &in_use, &|name| {
57
+ presentation.resolve_css_var(name).is_some()
58
+ })
59
+ }
60
+
61
+ /// The named values of `kind` a picker offers: the palette set's
62
+ /// entries, then any theme/page-authored host entries not already in
63
+ /// the set by name.
64
+ pub fn named_values(
65
+ workspace: &Workspace,
66
+ presentation: &Presentation,
67
+ kind: CssKind,
68
+ ) -> Vec<NamedValue> {
69
+ let mut out: Vec<NamedValue> = palette_set(workspace, presentation)
70
+ .into_iter()
71
+ .filter(|(name, _)| name.starts_with(kind.var_prefix()))
72
+ .map(|(name, value)| NamedValue { name, value })
73
+ .collect();
74
+
75
+ for entry in presentation.host_named_values(kind) {
76
+ if !out.iter().any(|x| x.name == entry.name) {
77
+ out.push(entry);
78
+ }
79
+ }
80
+
81
+ out
82
+ }
@@ -41,7 +41,9 @@ pub fn get_plugin_config_schema(
41
41
  let view_config_js =
42
42
  wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
43
43
  let raw = plugin._plugin_config_schema(&view_config_js)?;
44
- serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
44
+ serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
45
+ .map(|schema| schema.canonicalize_defaults())
46
+ .map_err(|e| e.into())
45
47
  }
46
48
 
47
49
  /// Queries the active plugin for the per-column [`ColumnConfigSchema`].
@@ -97,5 +99,7 @@ pub fn get_column_config_schema(
97
99
  &stats_js,
98
100
  )?;
99
101
 
100
- serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
102
+ serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
103
+ .map(|schema| schema.canonicalize_defaults())
104
+ .map_err(|e| e.into())
101
105
  }