@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
|
@@ -49,7 +49,7 @@ impl Renderer {
|
|
|
49
49
|
let active_plugin = self.active_plugin();
|
|
50
50
|
if let Some(plugin) = plugin.or(active_plugin.as_ref()) {
|
|
51
51
|
let theme_elem = plugin.unchecked_ref::<HtmlElement>();
|
|
52
|
-
match self.
|
|
52
|
+
match self.theme() {
|
|
53
53
|
Some(theme)
|
|
54
54
|
if theme_elem.get_attribute("theme").as_deref() != Some(theme.as_str()) =>
|
|
55
55
|
{
|
|
@@ -73,7 +73,7 @@ impl Renderer {
|
|
|
73
73
|
let _pin = self.pin_context(&guard, ctx.clone());
|
|
74
74
|
let plugin = self.ensure_plugin_selected()?;
|
|
75
75
|
let meta = self.metadata();
|
|
76
|
-
let stamped_theme = self.
|
|
76
|
+
let stamped_theme = self.theme();
|
|
77
77
|
self.stamp_theme(Some(&plugin));
|
|
78
78
|
self.stamp_active(&plugin);
|
|
79
79
|
plugin.restyle();
|
|
@@ -134,7 +134,19 @@ impl Renderer {
|
|
|
134
134
|
|
|
135
135
|
pub async fn resize(&self) -> ApiResult<()> {
|
|
136
136
|
self.0.geometry_cmd.set(Some(GeometryCmd::Measure));
|
|
137
|
-
|
|
137
|
+
|
|
138
|
+
// This caller queued a `Measure`, but the parked run it resolves
|
|
139
|
+
// through executes the LATEST `geometry_cmd` — which a concurrent
|
|
140
|
+
// presize sweep may have swapped to a `Presize` whose sweep
|
|
141
|
+
// coalesced (and so received `None`). Any present closure returned
|
|
142
|
+
// here is therefore held by no one else: invoke it, or the
|
|
143
|
+
// transport's present-hold leaks and the plot freezes on its
|
|
144
|
+
// last-blitted frame.
|
|
145
|
+
if let Some(present) = self.geometry_task().await? {
|
|
146
|
+
present.call0(&JsValue::NULL)?;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Ok(())
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
/// Public one-shot resize to explicit dimensions (`viewer.resize({
|
|
@@ -416,7 +428,7 @@ impl Renderer {
|
|
|
416
428
|
let viewer_elem = self.0.borrow().viewer_elem.clone();
|
|
417
429
|
let slot = self.slot_name();
|
|
418
430
|
let first_paint = !self.0.has_drawn.get();
|
|
419
|
-
let stamped_theme = self.
|
|
431
|
+
let stamped_theme = self.theme();
|
|
420
432
|
self.stamp_active(&plugin);
|
|
421
433
|
self.stamp_theme(Some(&plugin));
|
|
422
434
|
if self.needs_restyle() {
|
|
@@ -18,7 +18,7 @@ use std::collections::HashMap;
|
|
|
18
18
|
|
|
19
19
|
use futures::future::join_all;
|
|
20
20
|
use perspective_client::config::ViewConfig;
|
|
21
|
-
use perspective_js::utils::{ApiResult, JsValueSerdeExt};
|
|
21
|
+
use perspective_js::utils::{ApiError, ApiResult, JsValueSerdeExt};
|
|
22
22
|
use serde_json::Value;
|
|
23
23
|
use wasm_bindgen::prelude::*;
|
|
24
24
|
|
|
@@ -63,6 +63,50 @@ impl Renderer {
|
|
|
63
63
|
.unwrap_or_default()
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
fn resolve_css_refs_in(&self, column: &str, entry: &mut serde_json::Map<String, Value>) {
|
|
67
|
+
let host = self.borrow().viewer_elem.clone();
|
|
68
|
+
let lookup = |name: &str| crate::utils::read_custom_property(&host, name);
|
|
69
|
+
for (key, name) in resolve_css_refs(entry, &lookup) {
|
|
70
|
+
tracing::warn!(
|
|
71
|
+
"Dropping `columns_config[\"{column}\"].{key}`: `var({name})` is undefined or not \
|
|
72
|
+
a valid value of its kind"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Every CSS literal stored in the active plugin's per-column bucket
|
|
78
|
+
/// with its schema kind, sorted by `(column, key)`.
|
|
79
|
+
pub fn css_literals_in_use(
|
|
80
|
+
&self,
|
|
81
|
+
view_config: &ViewConfig,
|
|
82
|
+
session: &Session,
|
|
83
|
+
) -> Vec<CssLiteralUse> {
|
|
84
|
+
let mut out = vec![];
|
|
85
|
+
for (column, entry) in self.all_columns_configs() {
|
|
86
|
+
let Ok(schema) =
|
|
87
|
+
self.query_column_config_schema(view_config, session, &column, Some(&entry))
|
|
88
|
+
else {
|
|
89
|
+
continue;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (key, value) in &entry {
|
|
93
|
+
if let (Some(kind), Some(literal)) = (schema.css_kind_of(key), value.as_str())
|
|
94
|
+
&& parse_var_ref(literal).is_none()
|
|
95
|
+
{
|
|
96
|
+
out.push(CssLiteralUse {
|
|
97
|
+
column: column.clone(),
|
|
98
|
+
key: key.clone(),
|
|
99
|
+
kind,
|
|
100
|
+
literal: literal.to_owned(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
out.sort_by(|a, b| (&a.column, &a.key).cmp(&(&b.column, &b.key)));
|
|
107
|
+
out
|
|
108
|
+
}
|
|
109
|
+
|
|
66
110
|
/// Restore-prep snapshot: like [`Self::all_columns_configs`], but
|
|
67
111
|
/// for each column also materializes any `ControlSpec::Number`
|
|
68
112
|
/// fields the schema declares with `include: true` that aren't
|
|
@@ -173,15 +217,15 @@ impl Renderer {
|
|
|
173
217
|
.cloned()
|
|
174
218
|
}
|
|
175
219
|
|
|
176
|
-
/// Wholesale update the active plugin's per-column config map
|
|
220
|
+
/// Wholesale update the active plugin's per-column config map.
|
|
177
221
|
pub fn update_columns_configs(
|
|
178
222
|
&self,
|
|
179
223
|
view_config: &ViewConfig,
|
|
180
224
|
session: &Session,
|
|
181
225
|
update: ColumnConfigUpdate,
|
|
182
|
-
) -> bool {
|
|
226
|
+
) -> ApiResult<bool> {
|
|
183
227
|
let Some(n) = self.active_plugin_name() else {
|
|
184
|
-
return false;
|
|
228
|
+
return Ok(false);
|
|
185
229
|
};
|
|
186
230
|
|
|
187
231
|
match update {
|
|
@@ -190,24 +234,31 @@ impl Renderer {
|
|
|
190
234
|
let bucket = st.plugin_states.entry(n).or_default();
|
|
191
235
|
let was_nonempty = !bucket.columns.is_empty();
|
|
192
236
|
bucket.columns.clear();
|
|
193
|
-
was_nonempty
|
|
237
|
+
Ok(was_nonempty)
|
|
194
238
|
},
|
|
195
|
-
OptionalUpdate::Missing => false,
|
|
239
|
+
OptionalUpdate::Missing => Ok(false),
|
|
196
240
|
OptionalUpdate::Update(map) => {
|
|
197
|
-
let stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> =
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
241
|
+
let mut stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> =
|
|
242
|
+
Vec::with_capacity(map.len());
|
|
243
|
+
for (col, mut cfg) in map {
|
|
244
|
+
if let Ok(schema) =
|
|
245
|
+
self.query_column_config_schema(view_config, session, &col, Some(&cfg))
|
|
246
|
+
{
|
|
247
|
+
let active = schema.active_keys();
|
|
248
|
+
cfg.retain(|k, _| active.contains(k));
|
|
249
|
+
let errors = normalize_css_values(&schema, &mut cfg);
|
|
250
|
+
if let Some((key, error)) = errors.first() {
|
|
251
|
+
return Err(ApiError::from(JsValue::from_str(&format!(
|
|
252
|
+
"Invalid `columns_config[\"{col}\"].{key}`: {error}"
|
|
253
|
+
))));
|
|
206
254
|
}
|
|
207
255
|
|
|
208
|
-
(col, cfg)
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
self.resolve_css_refs_in(&col, &mut cfg);
|
|
257
|
+
strip_default_values(&schema, &mut cfg);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
stripped.push((col, cfg));
|
|
261
|
+
}
|
|
211
262
|
|
|
212
263
|
let mut st = self.borrow_mut();
|
|
213
264
|
let bucket = st.plugin_states.entry(n).or_default();
|
|
@@ -226,7 +277,7 @@ impl Renderer {
|
|
|
226
277
|
}
|
|
227
278
|
}
|
|
228
279
|
|
|
229
|
-
changed
|
|
280
|
+
Ok(changed)
|
|
230
281
|
},
|
|
231
282
|
}
|
|
232
283
|
}
|
|
@@ -253,22 +304,45 @@ impl Renderer {
|
|
|
253
304
|
&column_name,
|
|
254
305
|
current_value.as_ref(),
|
|
255
306
|
) {
|
|
307
|
+
for (key, error) in normalize_css_values(&schema, &mut update.value) {
|
|
308
|
+
tracing::error!("Dropping `{column_name}`.`{key}`: {error}");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
self.resolve_css_refs_in(&column_name, &mut update.value);
|
|
256
312
|
strip_default_values(&schema, &mut update.value);
|
|
257
313
|
}
|
|
258
314
|
|
|
315
|
+
let next = {
|
|
316
|
+
let mut st = self.borrow_mut();
|
|
317
|
+
let bucket = st.plugin_states.entry(n.clone()).or_default();
|
|
318
|
+
let entry = bucket.columns.entry(column_name.clone()).or_default();
|
|
319
|
+
for k in &update.keys {
|
|
320
|
+
entry.remove(k);
|
|
321
|
+
}
|
|
322
|
+
for (k, v) in update.value {
|
|
323
|
+
if update.keys.contains(&k) {
|
|
324
|
+
entry.insert(k, v);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
entry.clone()
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let active = self
|
|
332
|
+
.query_column_config_schema(view_config, session, &column_name, Some(&next))
|
|
333
|
+
.ok()
|
|
334
|
+
.map(|schema| schema.active_keys());
|
|
335
|
+
|
|
259
336
|
let mut st = self.borrow_mut();
|
|
260
337
|
let bucket = st.plugin_states.entry(n).or_default();
|
|
261
|
-
let entry = bucket.columns.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if
|
|
267
|
-
|
|
338
|
+
if let Some(entry) = bucket.columns.get_mut(&column_name) {
|
|
339
|
+
if let Some(active) = &active {
|
|
340
|
+
entry.retain(|k, _| active.contains(k));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if entry.is_empty() {
|
|
344
|
+
bucket.columns.remove(&column_name);
|
|
268
345
|
}
|
|
269
|
-
}
|
|
270
|
-
if entry.is_empty() {
|
|
271
|
-
bucket.columns.remove(&column_name);
|
|
272
346
|
}
|
|
273
347
|
}
|
|
274
348
|
|
|
@@ -305,7 +379,9 @@ impl Renderer {
|
|
|
305
379
|
let plugin = self.ensure_plugin_selected()?;
|
|
306
380
|
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
307
381
|
let raw = plugin._plugin_config_schema(&view_config_js)?;
|
|
308
|
-
serde_wasm_bindgen::from_value(raw)
|
|
382
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
383
|
+
.map(|schema| schema.canonicalize_defaults())
|
|
384
|
+
.map_err(|e| e.into())
|
|
309
385
|
}
|
|
310
386
|
|
|
311
387
|
/// Per-column counterpart of [`query_plugin_config_schema`]. Used by
|
|
@@ -352,7 +428,9 @@ impl Renderer {
|
|
|
352
428
|
&stats_js,
|
|
353
429
|
)?;
|
|
354
430
|
|
|
355
|
-
serde_wasm_bindgen::from_value(raw)
|
|
431
|
+
serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
|
|
432
|
+
.map(|schema| schema.canonicalize_defaults())
|
|
433
|
+
.map_err(|e| e.into())
|
|
356
434
|
}
|
|
357
435
|
|
|
358
436
|
/// Wholesale update the active plugin's plugin-level config map.
|
|
@@ -365,9 +443,9 @@ impl Renderer {
|
|
|
365
443
|
&self,
|
|
366
444
|
view_config: &ViewConfig,
|
|
367
445
|
update: PluginConfigUpdate,
|
|
368
|
-
) -> bool {
|
|
446
|
+
) -> ApiResult<bool> {
|
|
369
447
|
let Some(n) = self.active_plugin_name() else {
|
|
370
|
-
return false;
|
|
448
|
+
return Ok(false);
|
|
371
449
|
};
|
|
372
450
|
|
|
373
451
|
let schema = self.query_plugin_config_schema(view_config).ok();
|
|
@@ -377,14 +455,21 @@ impl Renderer {
|
|
|
377
455
|
OptionalUpdate::SetDefault => {
|
|
378
456
|
let changed = !bucket.plugin.is_empty();
|
|
379
457
|
bucket.plugin.clear();
|
|
380
|
-
changed
|
|
458
|
+
Ok(changed)
|
|
381
459
|
},
|
|
382
|
-
OptionalUpdate::Missing => false,
|
|
460
|
+
OptionalUpdate::Missing => Ok(false),
|
|
383
461
|
OptionalUpdate::Update(mut map) => {
|
|
384
462
|
let mut changed = false;
|
|
385
463
|
if let Some(s) = &schema {
|
|
386
464
|
let active = s.active_keys();
|
|
387
465
|
map.retain(|k, _| active.contains(k));
|
|
466
|
+
let errors = normalize_css_values(s, &mut map);
|
|
467
|
+
if let Some((key, error)) = errors.first() {
|
|
468
|
+
return Err(ApiError::from(JsValue::from_str(&format!(
|
|
469
|
+
"Invalid `plugin_config.{key}`: {error}"
|
|
470
|
+
))));
|
|
471
|
+
}
|
|
472
|
+
|
|
388
473
|
map.retain(|key, value| {
|
|
389
474
|
let is_default = s
|
|
390
475
|
.fields
|
|
@@ -408,7 +493,7 @@ impl Renderer {
|
|
|
408
493
|
}
|
|
409
494
|
}
|
|
410
495
|
|
|
411
|
-
changed
|
|
496
|
+
Ok(changed)
|
|
412
497
|
},
|
|
413
498
|
}
|
|
414
499
|
}
|
|
@@ -424,6 +509,10 @@ impl Renderer {
|
|
|
424
509
|
};
|
|
425
510
|
|
|
426
511
|
if let Ok(schema) = self.query_plugin_config_schema(view_config) {
|
|
512
|
+
for (key, error) in normalize_css_values(&schema, &mut update.value) {
|
|
513
|
+
tracing::error!("Dropping `plugin_config`.`{key}`: {error}");
|
|
514
|
+
}
|
|
515
|
+
|
|
427
516
|
strip_default_values(&schema, &mut update.value);
|
|
428
517
|
}
|
|
429
518
|
|
|
@@ -446,6 +535,34 @@ impl Renderer {
|
|
|
446
535
|
}
|
|
447
536
|
}
|
|
448
537
|
|
|
538
|
+
fn normalize_css_values(
|
|
539
|
+
schema: &ColumnConfigSchema,
|
|
540
|
+
map: &mut serde_json::Map<String, serde_json::Value>,
|
|
541
|
+
) -> Vec<(String, String)> {
|
|
542
|
+
let mut errors = vec![];
|
|
543
|
+
for (key, value) in map.iter_mut() {
|
|
544
|
+
let Some(kind) = schema.css_kind_of(key) else {
|
|
545
|
+
continue;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
let canonical = match value.as_str() {
|
|
549
|
+
Some(src) => kind.canonicalize(src),
|
|
550
|
+
None => Err(format!("expected a CSS string, got `{value}`")),
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
match canonical {
|
|
554
|
+
Ok(canonical) => *value = Value::String(canonical),
|
|
555
|
+
Err(error) => errors.push((key.clone(), error)),
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (key, _) in &errors {
|
|
560
|
+
map.remove(key);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
errors
|
|
564
|
+
}
|
|
565
|
+
|
|
449
566
|
fn strip_default_values(
|
|
450
567
|
schema: &ColumnConfigSchema,
|
|
451
568
|
map: &mut serde_json::Map<String, serde_json::Value>,
|
|
@@ -479,17 +596,162 @@ fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> boo
|
|
|
479
596
|
} if k == key => value.as_str() == Some(default.as_str()),
|
|
480
597
|
ControlSpec::Color {
|
|
481
598
|
key: k, default, ..
|
|
482
|
-
} if k == key =>
|
|
483
|
-
ControlSpec::
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
key_neg,
|
|
490
|
-
default_neg,
|
|
491
|
-
..
|
|
492
|
-
} if key_neg == key => value.as_str() == Some(default_neg.as_str()),
|
|
599
|
+
} if k == key => css_matches(CssKind::Color, value, default),
|
|
600
|
+
ControlSpec::Palette {
|
|
601
|
+
key: k, default, ..
|
|
602
|
+
} if k == key => css_matches(CssKind::Palette, value, default),
|
|
603
|
+
ControlSpec::GradientStops {
|
|
604
|
+
key: k, default, ..
|
|
605
|
+
} if k == key => css_matches(CssKind::Gradient, value, default),
|
|
493
606
|
_ => false,
|
|
494
607
|
}
|
|
495
608
|
}
|
|
609
|
+
|
|
610
|
+
fn css_matches(kind: CssKind, value: &Value, default: &str) -> bool {
|
|
611
|
+
value
|
|
612
|
+
.as_str()
|
|
613
|
+
.and_then(|src| kind.canonicalize(src).ok())
|
|
614
|
+
.is_some_and(|canonical| canonical == default)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[cfg(test)]
|
|
618
|
+
mod tests {
|
|
619
|
+
use serde_json::json;
|
|
620
|
+
|
|
621
|
+
use super::*;
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
fn palette_default_matches_canonically() {
|
|
625
|
+
let spec = ControlSpec::Palette {
|
|
626
|
+
key: "palette".to_owned(),
|
|
627
|
+
default: "linear-gradient(to right, #0366d6, #ff7f0e)".to_owned(),
|
|
628
|
+
max: None,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
assert!(matches_declared_default(
|
|
632
|
+
&spec,
|
|
633
|
+
"palette",
|
|
634
|
+
&json!("linear-gradient(to right, #0366d6, #ff7f0e)")
|
|
635
|
+
));
|
|
636
|
+
|
|
637
|
+
assert!(matches_declared_default(
|
|
638
|
+
&spec,
|
|
639
|
+
"palette",
|
|
640
|
+
&json!("linear-gradient(90deg, RGB(3,102,214), #FF7F0E)")
|
|
641
|
+
));
|
|
642
|
+
|
|
643
|
+
assert!(!matches_declared_default(
|
|
644
|
+
&spec,
|
|
645
|
+
"palette",
|
|
646
|
+
&json!("linear-gradient(to right, #ff7f0e, #0366d6)")
|
|
647
|
+
));
|
|
648
|
+
|
|
649
|
+
assert!(!matches_declared_default(
|
|
650
|
+
&spec,
|
|
651
|
+
"palette",
|
|
652
|
+
&json!("var(--psp-user--palette-1)")
|
|
653
|
+
));
|
|
654
|
+
|
|
655
|
+
assert!(!matches_declared_default(
|
|
656
|
+
&spec,
|
|
657
|
+
"palette",
|
|
658
|
+
&json!(["#0366d6"])
|
|
659
|
+
));
|
|
660
|
+
assert!(!matches_declared_default(
|
|
661
|
+
&spec,
|
|
662
|
+
"other",
|
|
663
|
+
&json!("linear-gradient(to right, #0366d6, #ff7f0e)")
|
|
664
|
+
));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#[test]
|
|
668
|
+
fn gradient_default_matches_canonical_values() {
|
|
669
|
+
let spec = ControlSpec::GradientStops {
|
|
670
|
+
key: "gradient".to_owned(),
|
|
671
|
+
default: "linear-gradient(to right, #0366d6 0%, #ff7f0e 33.3%)".to_owned(),
|
|
672
|
+
discrete: false,
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
assert!(matches_declared_default(
|
|
676
|
+
&spec,
|
|
677
|
+
"gradient",
|
|
678
|
+
&json!("linear-gradient(#0366d6, #ff7f0e 33.3%)")
|
|
679
|
+
));
|
|
680
|
+
|
|
681
|
+
assert!(!matches_declared_default(
|
|
682
|
+
&spec,
|
|
683
|
+
"gradient",
|
|
684
|
+
&json!("linear-gradient(to right, #0366d6 0%, #ff7f0e 33.4%)")
|
|
685
|
+
));
|
|
686
|
+
|
|
687
|
+
let color = ControlSpec::Color {
|
|
688
|
+
key: "color".to_owned(),
|
|
689
|
+
default: "#ff7f0e".to_owned(),
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
assert!(matches_declared_default(&color, "color", &json!("#FF7F0E")));
|
|
693
|
+
assert!(matches_declared_default(
|
|
694
|
+
&color,
|
|
695
|
+
"color",
|
|
696
|
+
&json!("rgb(255, 127, 14)")
|
|
697
|
+
));
|
|
698
|
+
assert!(!matches_declared_default(
|
|
699
|
+
&color,
|
|
700
|
+
"color",
|
|
701
|
+
&json!("var(--psp-user--color-1)")
|
|
702
|
+
));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#[test]
|
|
706
|
+
fn normalize_css_values_canonicalizes_and_reports() {
|
|
707
|
+
let schema = ColumnConfigSchema {
|
|
708
|
+
fields: vec![
|
|
709
|
+
ControlSpec::Color {
|
|
710
|
+
key: "color".to_owned(),
|
|
711
|
+
default: "#000000".to_owned(),
|
|
712
|
+
},
|
|
713
|
+
ControlSpec::Palette {
|
|
714
|
+
key: "palette".to_owned(),
|
|
715
|
+
default: "linear-gradient(to right, #000000)".to_owned(),
|
|
716
|
+
max: None,
|
|
717
|
+
},
|
|
718
|
+
ControlSpec::GradientStops {
|
|
719
|
+
key: "gradient".to_owned(),
|
|
720
|
+
default: "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
|
|
721
|
+
discrete: false,
|
|
722
|
+
},
|
|
723
|
+
ControlSpec::Bool {
|
|
724
|
+
key: "flag".to_owned(),
|
|
725
|
+
default: false,
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
let mut map = json!({
|
|
731
|
+
"color": "RGB(255,0,0)",
|
|
732
|
+
"palette": "var(--psp-user--palette-warm)",
|
|
733
|
+
"gradient": [{ "color": "#000000", "offset": 0 }],
|
|
734
|
+
"flag": true,
|
|
735
|
+
"foreign": "linear-gradient(red, blue)",
|
|
736
|
+
})
|
|
737
|
+
.as_object()
|
|
738
|
+
.unwrap()
|
|
739
|
+
.clone();
|
|
740
|
+
|
|
741
|
+
let errors = normalize_css_values(&schema, &mut map);
|
|
742
|
+
assert_eq!(errors.len(), 1);
|
|
743
|
+
assert_eq!(errors[0].0, "gradient");
|
|
744
|
+
assert_eq!(
|
|
745
|
+
map,
|
|
746
|
+
json!({
|
|
747
|
+
"color": "#ff0000",
|
|
748
|
+
"palette": "var(--psp-user--palette-warm)",
|
|
749
|
+
"flag": true,
|
|
750
|
+
"foreign": "linear-gradient(red, blue)",
|
|
751
|
+
})
|
|
752
|
+
.as_object()
|
|
753
|
+
.unwrap()
|
|
754
|
+
.clone()
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
package/src/rust/renderer.rs
CHANGED
|
@@ -142,19 +142,14 @@ pub struct RendererData {
|
|
|
142
142
|
/// coexist there.
|
|
143
143
|
slot_name: RefCell<Option<String>>,
|
|
144
144
|
|
|
145
|
-
/// This panel's
|
|
146
|
-
///
|
|
145
|
+
/// This panel's theme name — CONCRETE, resolved once at creation from
|
|
146
|
+
/// the config's `theme`, else the host's, else the registry default.
|
|
147
|
+
/// There is no "unthemed panel" that resolves against live element
|
|
148
|
+
/// state at draw time: a registry re-ordering must never repaint a
|
|
149
|
+
/// panel, so nothing but an explicit write may change this. `None` only
|
|
150
|
+
/// while no themes exist at all.
|
|
147
151
|
theme: RefCell<Option<String>>,
|
|
148
152
|
|
|
149
|
-
/// The registry default theme name (first registered), cached here so
|
|
150
|
-
/// LOCKED draw paths can resolve this panel's EFFECTIVE theme
|
|
151
|
-
/// synchronously (`Presentation::get_default_theme_name` is async — it
|
|
152
|
-
/// awaits theme discovery). Seeded by every content-load path before its
|
|
153
|
-
/// first locked draw (`restore_and_render`, `load()`, the
|
|
154
|
-
/// resize-observer's deferred first render) and kept fresh by the root's
|
|
155
|
-
/// `UpdatePresentation` default-theme fan-out and `resetThemes`.
|
|
156
|
-
default_theme: RefCell<Option<String>>,
|
|
157
|
-
|
|
158
153
|
/// Whether the active plugin has completed a draw. An EXPLICIT flag —
|
|
159
154
|
/// not inferred from DOM connectedness — because plugin elements may be
|
|
160
155
|
/// mounted eagerly (at panel creation / draw start, before the view
|
|
@@ -276,7 +271,6 @@ impl Renderer {
|
|
|
276
271
|
on_render_limits_changed: Default::default(),
|
|
277
272
|
slot_name: Default::default(),
|
|
278
273
|
theme: Default::default(),
|
|
279
|
-
default_theme: Default::default(),
|
|
280
274
|
has_drawn: Cell::new(false),
|
|
281
275
|
captured_theme: Default::default(),
|
|
282
276
|
cached_context: Default::default(),
|
|
@@ -299,45 +293,24 @@ impl Renderer {
|
|
|
299
293
|
self.0.slot_name.borrow().clone()
|
|
300
294
|
}
|
|
301
295
|
|
|
302
|
-
/// Set this panel's theme name
|
|
296
|
+
/// Set this panel's theme name. Callers must pass a CONCRETE name —
|
|
297
|
+
/// resolve "the default" through `Presentation` before calling, never by
|
|
298
|
+
/// leaving this empty.
|
|
303
299
|
pub fn set_theme(&self, name: Option<String>) {
|
|
304
300
|
*self.0.theme.borrow_mut() = name;
|
|
305
301
|
}
|
|
306
302
|
|
|
307
|
-
/// This panel's
|
|
303
|
+
/// This panel's theme name.
|
|
308
304
|
pub fn theme(&self) -> Option<String> {
|
|
309
305
|
self.0.theme.borrow().clone()
|
|
310
306
|
}
|
|
311
307
|
|
|
312
|
-
/// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`]
|
|
313
|
-
///
|
|
314
|
-
///
|
|
315
|
-
/// attribute-removal). The shared "stamp-with-commit" head of every
|
|
316
|
-
/// per-panel theme mutation site.
|
|
308
|
+
/// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] — the
|
|
309
|
+
/// shared "stamp-with-commit" head of every per-panel theme mutation
|
|
310
|
+
/// site.
|
|
317
311
|
pub fn set_theme_stamped(&self, theme: Option<String>) {
|
|
318
|
-
let stamp = theme.is_some() || self.default_theme().is_some();
|
|
319
312
|
self.set_theme(theme);
|
|
320
|
-
|
|
321
|
-
self.stamp_theme(None);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/// Set the cached registry default theme name (see the field docs on
|
|
326
|
-
/// [`RendererData`]).
|
|
327
|
-
pub fn set_default_theme(&self, name: Option<String>) {
|
|
328
|
-
*self.0.default_theme.borrow_mut() = name;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/// The cached registry default theme name.
|
|
332
|
-
pub fn default_theme(&self) -> Option<String> {
|
|
333
|
-
self.0.default_theme.borrow().clone()
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/// This panel's EFFECTIVE theme: its own ([`Self::theme`]), else the
|
|
337
|
-
/// cached registry default ([`Self::default_theme`]) — the value
|
|
338
|
-
/// [`Self::stamp_theme`] stamps.
|
|
339
|
-
pub fn effective_theme(&self) -> Option<String> {
|
|
340
|
-
self.theme().or_else(|| self.default_theme())
|
|
313
|
+
self.stamp_theme(None);
|
|
341
314
|
}
|
|
342
315
|
|
|
343
316
|
/// Whether the active plugin's captured `--psp-*` CSS is STALE — the
|
|
@@ -352,7 +325,7 @@ impl Renderer {
|
|
|
352
325
|
/// paint captures fresh by construction ("stamp before draw").
|
|
353
326
|
pub fn needs_restyle(&self) -> bool {
|
|
354
327
|
match &*self.0.captured_theme.borrow() {
|
|
355
|
-
Some(captured) => *captured != self.
|
|
328
|
+
Some(captured) => *captured != self.theme(),
|
|
356
329
|
None => false,
|
|
357
330
|
}
|
|
358
331
|
}
|
|
@@ -20,11 +20,16 @@ use crate::config::PluginStaticConfig;
|
|
|
20
20
|
|
|
21
21
|
#[extend::ext]
|
|
22
22
|
pub impl ViewConfigUpdate {
|
|
23
|
-
/// Coerce this update's `group_rollup_mode` to one
|
|
24
|
-
/// (`PluginStaticConfig
|
|
25
|
-
/// absent mode counts as `
|
|
26
|
-
///
|
|
27
|
-
/// when the update
|
|
23
|
+
/// Coerce this update's `group_rollup_mode` / `split_rollup_mode` to one
|
|
24
|
+
/// the plugin accepts (`PluginStaticConfig::*_rollup_modes`,
|
|
25
|
+
/// feature-filtered). An absent mode counts as `current`'s committed mode
|
|
26
|
+
/// for the acceptance check — `ViewConfig::apply_update` KEEPS the
|
|
27
|
+
/// committed mode when the update's field is `None`, so acceptance must
|
|
28
|
+
/// be judged against the mode that will actually be in effect. A
|
|
29
|
+
/// hardcoded-default fallback here let a plugin swap silently retain a
|
|
30
|
+
/// mode the new plugin excludes (e.g. Datagrid's `split_rollup_mode:
|
|
31
|
+
/// "rollup"` surviving a `restore({plugin: "Y Bar"})` onto a flat-only
|
|
32
|
+
/// chart).
|
|
28
33
|
///
|
|
29
34
|
/// Split out of [`Self::set_update_column_defaults`] so same-plugin
|
|
30
35
|
/// restores can enforce the mode WITHOUT the column-defaulting below —
|
|
@@ -33,6 +38,7 @@ pub impl ViewConfigUpdate {
|
|
|
33
38
|
fn set_update_rollup_defaults(
|
|
34
39
|
&mut self,
|
|
35
40
|
metadata: &SessionMetadata,
|
|
41
|
+
current: &ViewConfig,
|
|
36
42
|
config_static: &PluginStaticConfig,
|
|
37
43
|
) {
|
|
38
44
|
let rollup_features = metadata
|
|
@@ -44,7 +50,7 @@ pub impl ViewConfigUpdate {
|
|
|
44
50
|
if !group_rollups.contains(
|
|
45
51
|
self.group_rollup_mode
|
|
46
52
|
.as_ref()
|
|
47
|
-
.unwrap_or(&
|
|
53
|
+
.unwrap_or(¤t.group_rollup_mode),
|
|
48
54
|
) {
|
|
49
55
|
self.group_rollup_mode = group_rollups.first().cloned();
|
|
50
56
|
tracing::debug!(
|
|
@@ -62,7 +68,7 @@ pub impl ViewConfigUpdate {
|
|
|
62
68
|
if !split_rollups.contains(
|
|
63
69
|
self.split_rollup_mode
|
|
64
70
|
.as_ref()
|
|
65
|
-
.unwrap_or(&
|
|
71
|
+
.unwrap_or(¤t.split_rollup_mode),
|
|
66
72
|
) {
|
|
67
73
|
self.split_rollup_mode = split_rollups.first().cloned();
|
|
68
74
|
tracing::debug!(
|
|
@@ -81,10 +87,11 @@ pub impl ViewConfigUpdate {
|
|
|
81
87
|
fn set_update_column_defaults(
|
|
82
88
|
&mut self,
|
|
83
89
|
metadata: &SessionMetadata,
|
|
90
|
+
current: &ViewConfig,
|
|
84
91
|
columns: &[Option<String>],
|
|
85
92
|
config_static: &PluginStaticConfig,
|
|
86
93
|
) {
|
|
87
|
-
self.set_update_rollup_defaults(metadata, config_static);
|
|
94
|
+
self.set_update_rollup_defaults(metadata, current, config_static);
|
|
88
95
|
|
|
89
96
|
if let (None, Some(min_cols)) = (&self.columns, config_static.min_config_columns) {
|
|
90
97
|
let names_len = config_static.config_column_names.len();
|