@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.
- package/dist/cdn/perspective-viewer.js +2 -2
- package/dist/cdn/perspective-viewer.js.map +2 -2
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/perspective-viewer.d.ts +1 -0
- package/dist/esm/perspective-viewer.inline.js +9 -9
- package/dist/esm/perspective-viewer.inline.js.map +2 -2
- package/dist/esm/perspective-viewer.js +4 -4
- package/dist/esm/perspective-viewer.js.map +2 -2
- package/dist/esm/plugin.d.ts +10 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
- package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
- package/dist/wasm/perspective-viewer.d.ts +43 -36
- package/dist/wasm/perspective-viewer.js +194 -136
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
- package/package.json +1 -1
- package/src/css/column-settings-panel.css +9 -5
- package/src/css/column-style.css +209 -31
- package/src/css/dom/select.css +2 -1
- package/src/css/form/code-editor.css +4 -4
- package/src/rust/agent/tools.rs +48 -17
- package/src/rust/components/column_selector/active_column.rs +2 -2
- package/src/rust/components/column_selector/add_expression_button.rs +3 -3
- package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
- package/src/rust/components/column_selector/config_selector.rs +8 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
- package/src/rust/components/column_selector/inactive_column.rs +2 -2
- package/src/rust/components/column_selector.rs +48 -4
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
- package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
- package/src/rust/components/column_settings_sidebar.rs +88 -69
- package/src/rust/components/editable_header.rs +85 -144
- package/src/rust/components/expression_editor.rs +9 -16
- package/src/rust/components/form/code_editor.rs +16 -6
- package/src/rust/components/form/debug.rs +1 -0
- package/src/rust/components/form/mod.rs +3 -1
- package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
- package/src/rust/components/form/named_value_picker.rs +86 -0
- package/src/rust/components/form/palette_selector.rs +440 -0
- package/src/rust/components/main_panel/msg.rs +2 -2
- package/src/rust/components/main_panel/reconcile.rs +4 -4
- package/src/rust/components/plugin_tab.rs +4 -28
- package/src/rust/components/settings_panel.rs +25 -20
- package/src/rust/components/viewer/msg.rs +46 -7
- package/src/rust/components/viewer/panels.rs +11 -9
- package/src/rust/components/viewer/render.rs +8 -3
- package/src/rust/components/viewer/settings.rs +163 -23
- package/src/rust/components/viewer/snapshots.rs +46 -21
- package/src/rust/components/viewer/wiring.rs +14 -25
- package/src/rust/components/viewer.rs +12 -8
- package/src/rust/config/column_config_schema.rs +100 -41
- package/src/rust/config/css_scale.rs +882 -0
- package/src/rust/config/mod.rs +2 -0
- package/src/rust/config/options.rs +10 -0
- package/src/rust/config/viewer_config.rs +49 -55
- package/src/rust/config/workspace_config.rs +132 -5
- package/src/rust/custom_elements/viewer.rs +214 -155
- package/src/rust/custom_events.rs +14 -1
- package/src/rust/js/regular_layout.rs +3 -3
- package/src/rust/lib.rs +2 -0
- package/src/rust/presentation/column_locator.rs +19 -5
- package/src/rust/presentation.rs +192 -36
- package/src/rust/queries/column_locator.rs +74 -44
- package/src/rust/queries/get_viewer_config.rs +0 -3
- package/src/rust/queries/mod.rs +2 -0
- package/src/rust/queries/palette_set.rs +82 -0
- package/src/rust/queries/plugin_column_styles.rs +6 -2
- package/src/rust/renderer/dispatch.rs +16 -4
- package/src/rust/renderer/plugin_config.rs +309 -47
- package/src/rust/renderer.rs +15 -42
- package/src/rust/session/column_defaults_update.rs +15 -8
- package/src/rust/session/metadata.rs +5 -1
- package/src/rust/session/props.rs +1 -0
- package/src/rust/session.rs +9 -6
- package/src/rust/tasks/create_panel.rs +44 -57
- package/src/rust/tasks/edit_expression.rs +19 -23
- package/src/rust/tasks/edit_window.rs +8 -10
- package/src/rust/tasks/eject.rs +4 -1
- package/src/rust/tasks/pipeline.rs +2 -2
- package/src/rust/tasks/presize_panels.rs +44 -5
- package/src/rust/tasks/reset_all.rs +4 -4
- package/src/rust/tasks/restore_and_render.rs +17 -44
- package/src/rust/tasks/restore_panel.rs +11 -13
- package/src/rust/tasks/set_edit_mode.rs +3 -1
- package/src/rust/tasks/sync_update_panels.rs +11 -1
- package/src/rust/tasks/update_theme.rs +23 -18
- package/src/rust/utils/css_vars.rs +26 -0
- package/src/rust/utils/mod.rs +2 -0
- package/src/rust/utils/spawn.rs +1 -1
- package/src/rust/workspace.rs +435 -272
- package/src/svg/lock.svg +7 -0
- package/src/themes/botanical.css +10 -0
- package/src/themes/defaults.css +12 -0
- package/src/themes/dracula.css +17 -0
- package/src/themes/gruvbox-dark.css +20 -0
- package/src/themes/gruvbox.css +10 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/monokai.css +22 -0
- package/src/themes/phosphor.css +5 -0
- package/src/themes/pro-dark.css +10 -0
- package/src/themes/solarized-dark.css +7 -0
- package/src/themes/solarized.css +7 -0
- package/src/themes/vaporwave.css +20 -0
- package/src/ts/perspective-viewer.ts +1 -0
- package/src/ts/plugin.ts +14 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
- package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +1 -1
- package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
- package/src/rust/components/form/color_range_selector.rs +0 -137
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
- /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
|
|
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.
|
|
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
|
|
package/src/rust/presentation.rs
CHANGED
|
@@ -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::{
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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(¤t, &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
|
|
319
|
-
///
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
///
|
|
20
|
-
///
|
|
21
|
-
pub fn
|
|
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.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
49
|
+
metadata: &SessionMetadata,
|
|
47
50
|
) -> Option<ColumnLocator> {
|
|
48
|
-
open_column_settings
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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())
|
package/src/rust/queries/mod.rs
CHANGED
|
@@ -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)
|
|
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)
|
|
102
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
103
|
+
.map(|schema| schema.canonicalize_defaults())
|
|
104
|
+
.map_err(|e| e.into())
|
|
101
105
|
}
|