@perspective-dev/viewer 5.2.0 → 5.3.1
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 +8 -8
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -0
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -0
- 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/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/ledger.css +1 -0
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.css +1 -0
- 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/css/velvet.css +1 -0
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.d.ts +16 -3
- 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 +3 -3
- package/dist/esm/perspective-viewer.js +8 -8
- package/dist/esm/perspective-viewer.js.map +3 -3
- 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 +198 -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 +28 -5
- package/src/css/column-style.css +291 -31
- package/src/css/containers/tabs.css +13 -3
- package/src/css/dom/select.css +21 -9
- package/src/css/form/code-editor.css +4 -4
- package/src/css/plugin-selector.css +1 -0
- package/src/css/plugin-settings-panel.css +72 -0
- package/src/css/viewer.css +0 -5
- package/src/rust/agent/tools.rs +53 -19
- 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 +330 -234
- package/src/rust/components/column_settings_sidebar.rs +88 -69
- package/src/rust/components/containers/control_group.rs +91 -0
- package/src/rust/components/containers/mod.rs +1 -0
- package/src/rust/components/containers/select.rs +8 -2
- package/src/rust/components/datetime_column_style/simple.rs +15 -4
- package/src/rust/components/datetime_column_style.rs +50 -10
- 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 +126 -112
- package/src/rust/components/settings_panel.rs +26 -20
- package/src/rust/components/status_bar.rs +0 -1
- package/src/rust/components/style_controls/number_string_format/digits_section.rs +22 -10
- package/src/rust/components/style_controls/number_string_format/misc_section.rs +6 -2
- package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -0
- package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
- package/src/rust/components/style_controls/number_string_format.rs +37 -14
- 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 +435 -45
- package/src/rust/config/css_scale.rs +882 -0
- package/src/rust/config/datetime_column_style.rs +4 -21
- package/src/rust/config/mod.rs +2 -0
- package/src/rust/config/number_string_format.rs +211 -18
- 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 +209 -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 +50 -12
- package/src/rust/renderer/plugin_config.rs +362 -53
- 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/blueprint.css +190 -0
- package/src/themes/botanical.css +14 -17
- package/src/themes/defaults.css +4 -110
- package/src/themes/dracula.css +19 -14
- package/src/themes/eggplant.css +189 -0
- package/src/themes/gruvbox-dark.css +23 -15
- package/src/themes/gruvbox.css +11 -1
- package/src/themes/icons.css +2 -2
- package/src/themes/intl/de.css +23 -1
- package/src/themes/intl/es.css +23 -1
- package/src/themes/intl/fr.css +23 -1
- package/src/themes/intl/ja.css +23 -1
- package/src/themes/intl/pt.css +23 -1
- package/src/themes/intl/zh.css +23 -1
- package/src/themes/intl.css +26 -4
- package/src/themes/ledger.css +195 -0
- package/src/themes/monokai.css +24 -14
- package/src/themes/nord.css +186 -0
- package/src/themes/phosphor.css +7 -14
- package/src/themes/pro-dark.css +12 -16
- package/src/themes/pro.css +118 -0
- package/src/themes/solarized-dark.css +9 -16
- package/src/themes/solarized.css +7 -0
- package/src/themes/themes.css +10 -5
- package/src/themes/vaporwave.css +22 -14
- package/src/themes/velvet.css +192 -0
- package/src/ts/column-format.ts +52 -2
- 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/src/svg/dropdown-selector-light.svg +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/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,10 +88,24 @@ 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>>,
|
|
91
106
|
|
|
107
|
+
collapsed_control_groups: RefCell<HashSet<String>>,
|
|
108
|
+
|
|
92
109
|
/// Drag/drop in-progress state. Empty (`NoDrag`) when no user drag is
|
|
93
110
|
/// active. Mutated by `notify_drag_*` / `notify_drop`; read by component
|
|
94
111
|
/// CSS-class derivations (`is_dragover`, `get_drag_column`).
|
|
@@ -109,11 +126,20 @@ pub struct PresentationHandle {
|
|
|
109
126
|
/// dragged element from the shadow tree.
|
|
110
127
|
host_dragend: RefCell<Option<DragEndCallback>>,
|
|
111
128
|
|
|
129
|
+
source_dragend: RefCell<Option<(web_sys::EventTarget, DragEndCallback)>>,
|
|
130
|
+
|
|
112
131
|
/// IntersectionObserver-based fallback for the drag image, kept alive for
|
|
113
132
|
/// the duration of the drag.
|
|
114
133
|
drag_target: RefCell<Option<DragTargetState>>,
|
|
115
134
|
|
|
116
|
-
|
|
135
|
+
/// `(open, announce)` — `announce` says this toggle is the SOLE carrier
|
|
136
|
+
/// of the config change (a user gesture: toolbar, `toggleConfig`), so
|
|
137
|
+
/// the element must emit `toggle-settings` + a config-update for it. An
|
|
138
|
+
/// API restore's toggle is announced by its own view-config commit
|
|
139
|
+
/// dispatch instead — announcing here too would double-emit, the first
|
|
140
|
+
/// carrying the intermediate config (settings flipped, view fields not
|
|
141
|
+
/// yet committed).
|
|
142
|
+
pub settings_open_changed: PubSub<(bool, bool)>,
|
|
117
143
|
|
|
118
144
|
/// Injected callback from the root component, replacing the former
|
|
119
145
|
/// `is_workspace_changed: PubSub` field.
|
|
@@ -158,6 +184,8 @@ impl Presentation {
|
|
|
158
184
|
agent: Default::default(),
|
|
159
185
|
themes: Default::default(),
|
|
160
186
|
theme_init: Default::default(),
|
|
187
|
+
theme_selected: Cell::new(elem.get_attribute("theme").is_some()),
|
|
188
|
+
palette: Default::default(),
|
|
161
189
|
is_workspace: Default::default(),
|
|
162
190
|
settings_open_changed: Default::default(),
|
|
163
191
|
settings_before_open_changed: Default::default(),
|
|
@@ -165,6 +193,7 @@ impl Presentation {
|
|
|
165
193
|
on_is_workspace_changed: Default::default(),
|
|
166
194
|
is_settings_open: Default::default(),
|
|
167
195
|
open_column_settings: Default::default(),
|
|
196
|
+
collapsed_control_groups: Default::default(),
|
|
168
197
|
theme_config_updated: PubSub::default(),
|
|
169
198
|
on_eject: PubSub::default(),
|
|
170
199
|
statusbar_pointer_event: PubSub::default(),
|
|
@@ -173,6 +202,7 @@ impl Presentation {
|
|
|
173
202
|
on_dragstart: Default::default(),
|
|
174
203
|
on_dragend: Default::default(),
|
|
175
204
|
host_dragend: Default::default(),
|
|
205
|
+
source_dragend: Default::default(),
|
|
176
206
|
drag_target: Default::default(),
|
|
177
207
|
}));
|
|
178
208
|
|
|
@@ -238,13 +268,84 @@ impl Presentation {
|
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
|
|
241
|
-
|
|
242
|
-
|
|
271
|
+
/// See [`PresentationHandle::settings_open_changed`] for `announce`.
|
|
272
|
+
pub fn set_settings_open(&self, open: bool, announce: bool) {
|
|
273
|
+
self.settings_open_changed.emit((open, announce));
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
/// Sets the currently opened column settings. Emits an internal event on
|
|
246
277
|
/// change. Passing None is a shorthand for setting all fields to
|
|
247
278
|
/// None.
|
|
279
|
+
/// The workspace palette as last restored (canonical values).
|
|
280
|
+
pub fn palette(&self) -> BTreeMap<String, String> {
|
|
281
|
+
self.0.palette.borrow().clone()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Replace the host palette: every previously-applied `--psp-user--*`
|
|
285
|
+
/// inline property is removed, then `palette` is applied.
|
|
286
|
+
pub fn set_palette(&self, palette: BTreeMap<String, String>) -> ApiResult<()> {
|
|
287
|
+
let style = self.0.viewer_elem.style();
|
|
288
|
+
for name in self.0.palette.borrow().keys() {
|
|
289
|
+
style.remove_property(name)?;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (name, value) in &palette {
|
|
293
|
+
style.set_property(name, value)?;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
*self.0.palette.borrow_mut() = palette;
|
|
297
|
+
Ok(())
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// Pin `literal` into the restored palette, named by the same rules
|
|
301
|
+
/// as the derived set and applied to the host.
|
|
302
|
+
pub fn pin_style(&self, kind: CssKind, literal: &str) -> ApiResult<()> {
|
|
303
|
+
let Ok(value) = kind.canonicalize(literal) else {
|
|
304
|
+
return Ok(());
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
let current = self.palette();
|
|
308
|
+
let host = self.host_named_values(kind);
|
|
309
|
+
let set = assign_palette_names(¤t, &host, &[(kind, value)], &|name| {
|
|
310
|
+
self.resolve_css_var(name).is_some()
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
let style = self.0.viewer_elem.style();
|
|
314
|
+
for (name, value) in &set {
|
|
315
|
+
if !current.contains_key(name) {
|
|
316
|
+
style.set_property(name, value)?;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
*self.0.palette.borrow_mut() = set;
|
|
321
|
+
Ok(())
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// The host's computed value for custom property `name` (inline
|
|
325
|
+
/// palette, then theme/page stylesheets), or `None` if undefined.
|
|
326
|
+
pub fn resolve_css_var(&self, name: &str) -> Option<String> {
|
|
327
|
+
read_custom_property(&self.0.viewer_elem, name)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Theme/page-authored named values of `kind` on the host, canonical,
|
|
331
|
+
/// discovered by the contiguous-numbering walk `--psp-user--<kind>-1`,
|
|
332
|
+
/// `-2`, … up to the first undefined name.
|
|
333
|
+
pub fn host_named_values(&self, kind: CssKind) -> Vec<NamedValue> {
|
|
334
|
+
let mut out = vec![];
|
|
335
|
+
for n in 1.. {
|
|
336
|
+
let name = format!("{}{n}", kind.var_prefix());
|
|
337
|
+
let Some(raw) = self.resolve_css_var(&name) else {
|
|
338
|
+
break;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if let Ok(value) = kind.canonicalize(&raw) {
|
|
342
|
+
out.push(NamedValue { name, value });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
out
|
|
347
|
+
}
|
|
348
|
+
|
|
248
349
|
pub fn set_open_column_settings(&self, settings: Option<OpenColumnSettings>) {
|
|
249
350
|
let settings = settings.unwrap_or_default();
|
|
250
351
|
if *(self.open_column_settings.borrow()) != settings {
|
|
@@ -259,6 +360,20 @@ impl Presentation {
|
|
|
259
360
|
self.open_column_settings.borrow().deref().clone()
|
|
260
361
|
}
|
|
261
362
|
|
|
363
|
+
pub fn is_control_group_collapsed(&self, key: &str) -> bool {
|
|
364
|
+
self.collapsed_control_groups.borrow().contains(key)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
pub fn set_control_group_collapsed(&self, key: &str, collapsed: bool) {
|
|
368
|
+
if collapsed {
|
|
369
|
+
self.collapsed_control_groups
|
|
370
|
+
.borrow_mut()
|
|
371
|
+
.insert(key.to_owned());
|
|
372
|
+
} else {
|
|
373
|
+
self.collapsed_control_groups.borrow_mut().remove(key);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
262
377
|
async fn init(self) -> ApiResult<()> {
|
|
263
378
|
self.set_theme_attribute(self.get_selected_theme_name().await.as_deref())
|
|
264
379
|
}
|
|
@@ -315,22 +430,24 @@ impl Presentation {
|
|
|
315
430
|
index.and_then(|x| themes.get(x).cloned())
|
|
316
431
|
}
|
|
317
432
|
|
|
318
|
-
/// The
|
|
319
|
-
///
|
|
433
|
+
/// The theme a NEW panel is born with: the host's if it has one, else
|
|
434
|
+
/// the registry default. Synchronous, because panel creation is — the
|
|
435
|
+
/// registry fallback is `None` until the registry first parses, which
|
|
436
|
+
/// [`crate::tasks::seed_panel_theme`] fills in.
|
|
437
|
+
pub fn active_theme_name_sync(&self) -> Option<String> {
|
|
438
|
+
self.0
|
|
439
|
+
.viewer_elem
|
|
440
|
+
.get_attribute("theme")
|
|
441
|
+
.or_else(|| self.0.themes.borrow().as_ref()?.first().cloned())
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// The registry default — the FIRST registered theme, which a panel or
|
|
445
|
+
/// host resolves to only when it has no theme of its own. `None` if no
|
|
320
446
|
/// themes exist.
|
|
321
447
|
pub async fn get_default_theme_name(&self) -> Option<String> {
|
|
322
448
|
self.get_available_themes().await.ok()?.first().cloned()
|
|
323
449
|
}
|
|
324
450
|
|
|
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
451
|
fn set_theme_attribute(&self, theme: Option<&str>) -> ApiResult<()> {
|
|
335
452
|
if let Some(theme) = theme {
|
|
336
453
|
Ok(self.0.viewer_elem.set_attribute("theme", theme)?)
|
|
@@ -341,12 +458,39 @@ impl Presentation {
|
|
|
341
458
|
|
|
342
459
|
pub async fn reset_theme(&self) -> ApiResult<()> {
|
|
343
460
|
*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?;
|
|
461
|
+
self.set_theme_name(None).await?;
|
|
347
462
|
Ok(())
|
|
348
463
|
}
|
|
349
464
|
|
|
465
|
+
/// Adopt `themes` as the available set, KEEPING the host's theme unless
|
|
466
|
+
/// it was never explicitly chosen or has left the set — the only two
|
|
467
|
+
/// cases in which re-ordering the registry may move the viewer.
|
|
468
|
+
///
|
|
469
|
+
/// Always re-stamps; the caller publishes ([`Self::publish_theme_config`])
|
|
470
|
+
/// once its restyles have resolved, because the available list has
|
|
471
|
+
/// changed even when the selection has not.
|
|
472
|
+
///
|
|
473
|
+
/// @param themes the new set, or `None` to re-parse the document.
|
|
474
|
+
///
|
|
475
|
+
/// # Returns
|
|
476
|
+
/// The active theme after the change.
|
|
477
|
+
pub async fn reset_themes(&self, themes: Option<Vec<String>>) -> ApiResult<Option<String>> {
|
|
478
|
+
let selected = self
|
|
479
|
+
.0
|
|
480
|
+
.theme_selected
|
|
481
|
+
.get()
|
|
482
|
+
.then(|| self.0.viewer_elem.get_attribute("theme"))
|
|
483
|
+
.flatten();
|
|
484
|
+
|
|
485
|
+
self.reset_available_themes(themes).await;
|
|
486
|
+
let available = self.get_available_themes().await?;
|
|
487
|
+
let kept = selected.filter(|name| available.contains(name));
|
|
488
|
+
self.0.theme_selected.set(kept.is_some());
|
|
489
|
+
let active = kept.or_else(|| available.first().cloned());
|
|
490
|
+
self.set_theme_attribute(active.as_deref())?;
|
|
491
|
+
Ok(active)
|
|
492
|
+
}
|
|
493
|
+
|
|
350
494
|
/// Set the theme by name, or `None` for the default theme.
|
|
351
495
|
///
|
|
352
496
|
/// A NAMED theme's host attribute write is SYNCHRONOUS ("stamp with
|
|
@@ -368,6 +512,7 @@ impl Presentation {
|
|
|
368
512
|
/// # Returns
|
|
369
513
|
/// A `bool` indicating whether the internal state changed.
|
|
370
514
|
pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult<bool> {
|
|
515
|
+
self.0.theme_selected.set(theme.is_some());
|
|
371
516
|
if let Some(theme) = theme {
|
|
372
517
|
if self.0.viewer_elem.get_attribute("theme").as_deref() == Some(theme) {
|
|
373
518
|
return Ok(false);
|
|
@@ -377,20 +522,27 @@ impl Presentation {
|
|
|
377
522
|
}
|
|
378
523
|
|
|
379
524
|
let themes = self.get_available_themes().await?;
|
|
380
|
-
|
|
381
|
-
themes.iter().position(|x| x == theme)
|
|
382
|
-
} else if !themes.is_empty() {
|
|
525
|
+
if theme.is_none() {
|
|
383
526
|
self.set_theme_attribute(themes.first().map(|x| x.as_str()))?;
|
|
384
|
-
|
|
385
|
-
} else {
|
|
386
|
-
self.set_theme_attribute(None)?;
|
|
387
|
-
None
|
|
388
|
-
};
|
|
527
|
+
}
|
|
389
528
|
|
|
390
|
-
self.theme_config_updated.emit((themes, index));
|
|
391
529
|
Ok(true)
|
|
392
530
|
}
|
|
393
531
|
|
|
532
|
+
/// Publish the `theme_config_updated` snapshot — the available themes
|
|
533
|
+
/// and the host's current selection — to the component tree.
|
|
534
|
+
pub async fn publish_theme_config(&self) -> ApiResult<()> {
|
|
535
|
+
let themes = self.get_available_themes().await?;
|
|
536
|
+
let index = self
|
|
537
|
+
.0
|
|
538
|
+
.viewer_elem
|
|
539
|
+
.get_attribute("theme")
|
|
540
|
+
.and_then(|active| themes.iter().position(|x| *x == active));
|
|
541
|
+
|
|
542
|
+
self.theme_config_updated.emit((themes, index));
|
|
543
|
+
Ok(())
|
|
544
|
+
}
|
|
545
|
+
|
|
394
546
|
/// Snapshot the drag state as a [`DragDropProps`] value for threading
|
|
395
547
|
/// through the component tree without PubSub subscriptions.
|
|
396
548
|
pub fn drag_drop_props(&self) -> DragDropProps {
|
|
@@ -427,6 +579,7 @@ impl Presentation {
|
|
|
427
579
|
|
|
428
580
|
pub fn set_drag_image(&self, event: &DragEvent) -> ApiResult<()> {
|
|
429
581
|
event.stop_propagation();
|
|
582
|
+
self.register_source_dragend(event)?;
|
|
430
583
|
if let Some(dt) = event.data_transfer() {
|
|
431
584
|
dt.set_drop_effect("move");
|
|
432
585
|
}
|
|
@@ -485,8 +638,7 @@ impl Presentation {
|
|
|
485
638
|
_ => None,
|
|
486
639
|
};
|
|
487
640
|
|
|
488
|
-
self.
|
|
489
|
-
*self.drag_state.borrow_mut() = DragState::NoDrag;
|
|
641
|
+
self.end_drag();
|
|
490
642
|
if let Some(action) = action {
|
|
491
643
|
self.drop_received.emit(action);
|
|
492
644
|
}
|
|
@@ -510,11 +662,15 @@ impl Presentation {
|
|
|
510
662
|
/// End the drag/drop action by resetting the state to default.
|
|
511
663
|
pub fn notify_drag_end(&self) {
|
|
512
664
|
if self.drag_state.borrow().is_drag_in_progress() {
|
|
513
|
-
self.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
665
|
+
self.end_drag();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
fn end_drag(&self) {
|
|
670
|
+
self.drag_target.borrow_mut().take();
|
|
671
|
+
*self.drag_state.borrow_mut() = DragState::NoDrag;
|
|
672
|
+
if let Some(cb) = self.on_dragend.borrow().as_ref() {
|
|
673
|
+
cb.emit(());
|
|
518
674
|
}
|
|
519
675
|
}
|
|
520
676
|
|
|
@@ -522,6 +678,23 @@ impl Presentation {
|
|
|
522
678
|
/// element so that drag-end cleanup fires even when Yew re-renders
|
|
523
679
|
/// remove the original dragged element from the shadow DOM. The host
|
|
524
680
|
/// element is outside the virtual DOM and therefore stable.
|
|
681
|
+
fn register_source_dragend(&self, event: &DragEvent) -> ApiResult<()> {
|
|
682
|
+
let target = event.target().into_apierror()?;
|
|
683
|
+
if let Some((prev_target, prev)) = self.source_dragend.borrow_mut().take() {
|
|
684
|
+
let _ = prev_target
|
|
685
|
+
.remove_event_listener_with_callback("dragend", prev.as_ref().unchecked_ref());
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
let this = self.clone();
|
|
689
|
+
let closure = Closure::wrap(Box::new(move |_event: DragEvent| {
|
|
690
|
+
this.notify_drag_end();
|
|
691
|
+
}) as Box<dyn FnMut(DragEvent)>);
|
|
692
|
+
|
|
693
|
+
target.add_event_listener_with_callback("dragend", closure.as_ref().unchecked_ref())?;
|
|
694
|
+
*self.source_dragend.borrow_mut() = Some((target, closure));
|
|
695
|
+
Ok(())
|
|
696
|
+
}
|
|
697
|
+
|
|
525
698
|
fn register_host_dragend(&self) {
|
|
526
699
|
if let Some(prev) = self.host_dragend.borrow_mut().take() {
|
|
527
700
|
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
|
+
}
|