@perspective-dev/viewer 5.4.0 → 5.5.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 +4 -4
- package/dist/cdn/perspective-viewer.js.map +2 -2
- package/dist/css/blueprint.css +1 -1
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.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/intl/ar.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 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.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/css/velvet.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/perspective-viewer.d.ts +2 -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 +4 -4
- package/dist/esm/perspective-viewer.js.map +2 -2
- package/dist/esm/plugin.d.ts +9 -0
- package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
- package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
- package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
- package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
- package/dist/wasm/perspective-viewer.d.ts +91 -47
- package/dist/wasm/perspective-viewer.js +216 -136
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
- package/package.json +5 -5
- package/src/css/column-selector.css +11 -3
- package/src/css/column-style.css +162 -0
- package/src/css/config-selector.css +1 -1
- package/src/css/form/debug.css +9 -0
- package/src/css/status-bar.css +11 -0
- package/src/css/ui/containers/tabs.css +2 -1
- package/src/rust/agent/tools.rs +80 -7
- package/src/rust/components/column_selector/active_column.rs +9 -0
- package/src/rust/components/column_selector.rs +11 -0
- package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
- package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
- package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
- package/src/rust/components/debug_panel.rs +107 -50
- package/src/rust/components/main_panel/msg.rs +4 -0
- package/src/rust/components/main_panel.rs +14 -0
- package/src/rust/components/mod.rs +0 -1
- package/src/rust/components/plugin_tab.rs +57 -3
- package/src/rust/components/settings_panel.rs +6 -0
- package/src/rust/components/status_indicator.rs +4 -0
- package/src/rust/components/viewer/panels.rs +3 -0
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/wiring.rs +10 -1
- package/src/rust/config/column_config_schema.rs +312 -8
- package/src/rust/config/datetime_column_style.rs +0 -11
- package/src/rust/config/mod.rs +0 -2
- package/src/rust/config/options.rs +50 -0
- package/src/rust/config/workspace_config.rs +69 -24
- package/src/rust/custom_elements/viewer.rs +194 -97
- package/src/rust/js/plugin.rs +3 -1
- package/src/rust/lib.rs +7 -3
- package/src/rust/queries/plugin_column_styles.rs +6 -5
- package/src/rust/renderer/plugin_config.rs +200 -41
- package/src/rust/renderer/plugin_store.rs +2 -1
- package/src/rust/renderer/props.rs +5 -0
- package/src/rust/renderer.rs +9 -0
- package/src/rust/session/props.rs +4 -0
- package/src/rust/session.rs +151 -80
- package/src/rust/tasks/create_panel.rs +4 -1
- package/src/rust/tasks/pipeline.rs +18 -7
- package/src/rust/tasks/restore_panel.rs +129 -30
- package/src/rust/tasks/send_column_config.rs +3 -0
- package/src/rust/tasks/sync_update_panels.rs +147 -67
- package/src/rust/tasks/table_lifecycle.rs +7 -1
- package/src/rust/ui/containers/tab_list.rs +1 -1
- package/src/rust/ui/form/intl_label.rs +4 -23
- package/src/rust/ui/portal.rs +1 -0
- package/src/rust/utils/css_vars.rs +13 -0
- package/src/rust/utils/local_fonts.rs +395 -0
- package/src/rust/utils/mod.rs +2 -0
- package/src/rust/workspace.rs +12 -0
- package/src/svg/status_pending.svg +3 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +20 -7
- package/src/themes/intl/de.css +20 -7
- package/src/themes/intl/es.css +20 -7
- package/src/themes/intl/fr.css +20 -7
- package/src/themes/intl/ja.css +20 -7
- package/src/themes/intl/pt.css +20 -7
- package/src/themes/intl/zh.css +20 -7
- package/src/themes/intl.css +20 -7
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/plugin.ts +9 -0
- package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
- package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
- package/src/ts/ts-rs/GetViewOptions.ts +7 -0
- package/src/ts/ts-rs/RestoreOptions.ts +7 -1
- package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
- package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
- package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
- package/src/rust/components/string_column_style.rs +0 -120
- package/src/rust/config/datetime_column_style/color_mode.rs +0 -62
- package/src/rust/config/string_column_style.rs +0 -135
- package/src/ts/ts-rs/FormatMode.ts +0 -3
- package/src/ts/ts-rs/StringColorMode.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-ea71a3990cad35d7}/inline5.js +0 -0
|
@@ -250,6 +250,9 @@ pub struct MainPanel {
|
|
|
250
250
|
/// `Workspace::staged_changed` → [`MainPanelMsg::StagedChanged`] on THIS
|
|
251
251
|
/// component's scope (see the message doc for why not the root's).
|
|
252
252
|
_staged_sub: crate::utils::Subscription,
|
|
253
|
+
|
|
254
|
+
/// `Workspace::layout_staged` → [`MainPanelMsg::LayoutStaged`].
|
|
255
|
+
_layout_staged_sub: crate::utils::Subscription,
|
|
253
256
|
}
|
|
254
257
|
|
|
255
258
|
impl Component for MainPanel {
|
|
@@ -351,6 +354,15 @@ impl Component for MainPanel {
|
|
|
351
354
|
.add_listener(move |()| cb.emit(()))
|
|
352
355
|
};
|
|
353
356
|
|
|
357
|
+
let layout_staged_sub = {
|
|
358
|
+
use crate::utils::AddListener;
|
|
359
|
+
let cb = ctx.link().callback(|_: ()| MainPanelMsg::LayoutStaged);
|
|
360
|
+
ctx.props()
|
|
361
|
+
.workspace
|
|
362
|
+
.layout_staged()
|
|
363
|
+
.add_listener(move |()| cb.emit(()))
|
|
364
|
+
};
|
|
365
|
+
|
|
354
366
|
Self {
|
|
355
367
|
main_panel_ref: NodeRef::default(),
|
|
356
368
|
layout_ref: NodeRef::default(),
|
|
@@ -368,6 +380,7 @@ impl Component for MainPanel {
|
|
|
368
380
|
theme_backgrounds: HashMap::new(),
|
|
369
381
|
stamped_frame_themes: None,
|
|
370
382
|
_staged_sub: staged_sub,
|
|
383
|
+
_layout_staged_sub: layout_staged_sub,
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
386
|
|
|
@@ -375,6 +388,7 @@ impl Component for MainPanel {
|
|
|
375
388
|
match msg {
|
|
376
389
|
MainPanelMsg::PointerEvent(event) => self.on_pointer_event(ctx, event),
|
|
377
390
|
MainPanelMsg::StagedChanged => true,
|
|
391
|
+
MainPanelMsg::LayoutStaged => true,
|
|
378
392
|
MainPanelMsg::LayoutUpdated => self.on_layout_updated(ctx),
|
|
379
393
|
MainPanelMsg::TabSelected(name) => self.on_tab_selected(ctx, name),
|
|
380
394
|
MainPanelMsg::ContextMenu(id, x, y) => self.on_context_menu(ctx, id, x, y),
|
|
@@ -19,10 +19,12 @@ use itertools::Itertools;
|
|
|
19
19
|
use perspective_client::config::ViewConfig;
|
|
20
20
|
use yew::prelude::*;
|
|
21
21
|
|
|
22
|
+
use crate::components::column_settings_sidebar::style_tab::alignment_field::AlignmentField;
|
|
23
|
+
use crate::components::column_settings_sidebar::style_tab::font_field::FontField;
|
|
22
24
|
use crate::components::column_settings_sidebar::style_tab::primitive_field::{
|
|
23
25
|
BoolField, ColorField, EnumField, NumberFieldPrimitive,
|
|
24
26
|
};
|
|
25
|
-
use crate::config::ControlSpec;
|
|
27
|
+
use crate::config::{Alignment, ControlSpec, FontToggle};
|
|
26
28
|
use crate::presentation::Presentation;
|
|
27
29
|
use crate::queries::get_plugin_config_schema;
|
|
28
30
|
use crate::renderer::Renderer;
|
|
@@ -72,9 +74,10 @@ pub fn PluginTab(props: &PluginTabProps) -> Html {
|
|
|
72
74
|
let schema = {
|
|
73
75
|
let renderer = props.renderer.clone();
|
|
74
76
|
let view_config = props.view_config.clone();
|
|
77
|
+
let plugin_config = props.plugin_config.clone();
|
|
75
78
|
use_memo(
|
|
76
79
|
(props.plugin_config.clone(), props.view_config.clone()),
|
|
77
|
-
move |_| match get_plugin_config_schema(&renderer, &view_config) {
|
|
80
|
+
move |_| match get_plugin_config_schema(&renderer, &view_config, Some(&plugin_config)) {
|
|
78
81
|
Ok(schema) => schema.fields,
|
|
79
82
|
Err(error) => {
|
|
80
83
|
tracing::error!("{}", error);
|
|
@@ -180,6 +183,58 @@ fn render_leaf(
|
|
|
180
183
|
/>
|
|
181
184
|
})
|
|
182
185
|
},
|
|
186
|
+
ControlSpec::Font {
|
|
187
|
+
key,
|
|
188
|
+
default,
|
|
189
|
+
size,
|
|
190
|
+
bold,
|
|
191
|
+
italic,
|
|
192
|
+
} => {
|
|
193
|
+
let current = raw_config
|
|
194
|
+
.get(&key)
|
|
195
|
+
.and_then(|v| v.as_str().map(|s| s.to_string()));
|
|
196
|
+
let toggle = |t: &FontToggle| raw_config.get(&t.key).and_then(|v| v.as_bool());
|
|
197
|
+
let size_current = size
|
|
198
|
+
.as_ref()
|
|
199
|
+
.and_then(|s| raw_config.get(&s.key))
|
|
200
|
+
.and_then(|v| v.as_f64());
|
|
201
|
+
let bold_current = bold.as_ref().and_then(toggle);
|
|
202
|
+
let italic_current = italic.as_ref().and_then(toggle);
|
|
203
|
+
Some(html! {
|
|
204
|
+
<FontField
|
|
205
|
+
field_key={key}
|
|
206
|
+
{default}
|
|
207
|
+
{current}
|
|
208
|
+
{size}
|
|
209
|
+
{size_current}
|
|
210
|
+
{bold}
|
|
211
|
+
{bold_current}
|
|
212
|
+
{italic}
|
|
213
|
+
{italic_current}
|
|
214
|
+
on_change={on_change.clone()}
|
|
215
|
+
/>
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
ControlSpec::Alignment {
|
|
219
|
+
key,
|
|
220
|
+
default,
|
|
221
|
+
corners,
|
|
222
|
+
} => {
|
|
223
|
+
let current = raw_config
|
|
224
|
+
.get(&key)
|
|
225
|
+
.and_then(|v| v.as_str())
|
|
226
|
+
.and_then(Alignment::parse);
|
|
227
|
+
|
|
228
|
+
Some(html! {
|
|
229
|
+
<AlignmentField
|
|
230
|
+
field_key={key}
|
|
231
|
+
{default}
|
|
232
|
+
{corners}
|
|
233
|
+
{current}
|
|
234
|
+
on_change={on_change.clone()}
|
|
235
|
+
/>
|
|
236
|
+
})
|
|
237
|
+
},
|
|
183
238
|
ControlSpec::Bool { key, default } => {
|
|
184
239
|
let current = raw_config.get(&key).and_then(|v| v.as_bool());
|
|
185
240
|
Some(html! {
|
|
@@ -220,7 +275,6 @@ fn render_leaf(
|
|
|
220
275
|
| ControlSpec::AggregateDepth
|
|
221
276
|
| ControlSpec::NumberSeriesStyle { .. }
|
|
222
277
|
| ControlSpec::DatetimeFormat { .. }
|
|
223
|
-
| ControlSpec::StringFormat
|
|
224
278
|
| ControlSpec::Symbols { .. }
|
|
225
279
|
| ControlSpec::NumberFormat { .. }
|
|
226
280
|
| ControlSpec::String { .. }
|
|
@@ -60,6 +60,10 @@ pub struct SettingsPanelProps {
|
|
|
60
60
|
/// prop-driven instead of reading `Renderer` directly.
|
|
61
61
|
pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
|
|
62
62
|
|
|
63
|
+
/// Snapshot of the active plugin's `columns_config` bucket, threaded
|
|
64
|
+
/// from `RendererProps` into `ColumnSelector`.
|
|
65
|
+
pub columns_config: PtrEqRc<ColumnConfigMap>,
|
|
66
|
+
|
|
63
67
|
/// Column currently being dragged (if any) — threaded to show drag
|
|
64
68
|
/// highlights without per-component `DragDrop` PubSub subscriptions.
|
|
65
69
|
pub drag_column: Option<String>,
|
|
@@ -114,6 +118,7 @@ impl PartialEq for SettingsPanelProps {
|
|
|
114
118
|
&& self.plugin_static_config == rhs.plugin_static_config
|
|
115
119
|
&& self.view_config == rhs.view_config
|
|
116
120
|
&& self.plugin_config == rhs.plugin_config
|
|
121
|
+
&& self.columns_config == rhs.columns_config
|
|
117
122
|
&& self.drag_column == rhs.drag_column
|
|
118
123
|
&& self.metadata == rhs.metadata
|
|
119
124
|
&& self.open_column_settings == rhs.open_column_settings
|
|
@@ -356,6 +361,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
|
|
|
356
361
|
named_column_count={props.named_column_count}
|
|
357
362
|
plugin_static_config={props.plugin_static_config.clone()}
|
|
358
363
|
view_config={props.view_config.clone()}
|
|
364
|
+
columns_config={props.columns_config.clone()}
|
|
359
365
|
drag_column={props.drag_column.clone()}
|
|
360
366
|
metadata={props.metadata.clone()}
|
|
361
367
|
selected_theme={props.selected_theme.clone()}
|
|
@@ -51,6 +51,8 @@ pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
|
|
|
51
51
|
&& matches!(props.session_props.has_table, Some(TableLoadState::Loading))
|
|
52
52
|
{
|
|
53
53
|
StatusIconState::Loading
|
|
54
|
+
} else if matches!(props.session_props.has_table, Some(TableLoadState::Pending)) {
|
|
55
|
+
StatusIconState::Pending
|
|
54
56
|
} else if props.update_count > 0 {
|
|
55
57
|
StatusIconState::Updating
|
|
56
58
|
} else if has_table_cells {
|
|
@@ -65,6 +67,7 @@ pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
|
|
|
65
67
|
StatusIconState::Normal => "connected",
|
|
66
68
|
StatusIconState::Updating => "updating",
|
|
67
69
|
StatusIconState::Loading => "loading",
|
|
70
|
+
StatusIconState::Pending => "pending",
|
|
68
71
|
StatusIconState::Uninitialized => "uninitialized",
|
|
69
72
|
};
|
|
70
73
|
|
|
@@ -107,6 +110,7 @@ pub fn StatusIndicator(props: &StatusIndicatorProps) -> Html {
|
|
|
107
110
|
#[derive(Clone, Debug, PartialEq)]
|
|
108
111
|
enum StatusIconState {
|
|
109
112
|
Loading,
|
|
113
|
+
Pending,
|
|
110
114
|
Updating,
|
|
111
115
|
Errored(String, String, &'static str, bool),
|
|
112
116
|
Normal,
|
|
@@ -235,6 +235,7 @@ impl PerspectiveViewer {
|
|
|
235
235
|
None,
|
|
236
236
|
update,
|
|
237
237
|
client,
|
|
238
|
+
crate::session::MissingTable::Error,
|
|
238
239
|
)
|
|
239
240
|
.await?;
|
|
240
241
|
|
|
@@ -269,6 +270,7 @@ impl PerspectiveViewer {
|
|
|
269
270
|
None,
|
|
270
271
|
ViewerConfigInitial::new(table_name),
|
|
271
272
|
client,
|
|
273
|
+
crate::session::MissingTable::Error,
|
|
272
274
|
)
|
|
273
275
|
.await?;
|
|
274
276
|
|
|
@@ -313,6 +315,7 @@ impl PerspectiveViewer {
|
|
|
313
315
|
None,
|
|
314
316
|
ViewerConfigInitial::new(table),
|
|
315
317
|
Some(client),
|
|
318
|
+
crate::session::MissingTable::Error,
|
|
316
319
|
)
|
|
317
320
|
.await?;
|
|
318
321
|
|
|
@@ -95,6 +95,7 @@ impl PerspectiveViewer {
|
|
|
95
95
|
plugin_static_config={self.renderer_props.config.clone()}
|
|
96
96
|
{view_config}
|
|
97
97
|
plugin_config={self.renderer_props.plugin_config.clone()}
|
|
98
|
+
columns_config={self.renderer_props.columns_config.clone()}
|
|
98
99
|
{drag_column}
|
|
99
100
|
metadata={metadata.clone()}
|
|
100
101
|
open_column_settings={self.presentation_props.open_column_settings.clone()}
|
|
@@ -304,12 +304,21 @@ pub(super) fn create_active_subscriptions(
|
|
|
304
304
|
}
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
+
let cb_columns_config = ctx.link().callback({
|
|
308
|
+
let renderer = renderer.clone();
|
|
309
|
+
move |_: ColumnConfigMap| UpdateRenderer(Box::new(renderer.to_props(None)))
|
|
310
|
+
});
|
|
311
|
+
|
|
307
312
|
let sub1 = renderer.plugin_changed.add_listener(cb_plugin);
|
|
308
313
|
let sub2 = renderer
|
|
309
314
|
.plugin_config_changed
|
|
310
315
|
.add_listener(cb_plugin_config);
|
|
311
316
|
|
|
312
|
-
|
|
317
|
+
let sub3 = renderer
|
|
318
|
+
.columns_config_changed
|
|
319
|
+
.add_listener(cb_columns_config);
|
|
320
|
+
|
|
321
|
+
vec![sub1, sub2, sub3]
|
|
313
322
|
};
|
|
314
323
|
|
|
315
324
|
let mut subscriptions = Vec::new();
|
|
@@ -74,6 +74,38 @@ pub enum ControlSpec {
|
|
|
74
74
|
variants: Vec<EnumVariant>,
|
|
75
75
|
default: String,
|
|
76
76
|
},
|
|
77
|
+
|
|
78
|
+
/// A CSS `font-family` picked from the generic keywords plus the families
|
|
79
|
+
/// the host can enumerate, with optional size, bold and italic inputs
|
|
80
|
+
/// each bound to its own key.
|
|
81
|
+
Font {
|
|
82
|
+
key: String,
|
|
83
|
+
default: String,
|
|
84
|
+
|
|
85
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
86
|
+
size: Option<FontSize>,
|
|
87
|
+
|
|
88
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
89
|
+
bold: Option<FontToggle>,
|
|
90
|
+
|
|
91
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
92
|
+
italic: Option<FontToggle>,
|
|
93
|
+
},
|
|
94
|
+
/// A 3×3 anchor picker whose value is one of the nine [`Alignment`]
|
|
95
|
+
/// tokens.
|
|
96
|
+
Alignment {
|
|
97
|
+
key: String,
|
|
98
|
+
|
|
99
|
+
/// The cell shown as the unmodified value and elided from serialized
|
|
100
|
+
/// configs, or `None` when an unset key stands for something no cell
|
|
101
|
+
/// can show.
|
|
102
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
103
|
+
default: Option<Alignment>,
|
|
104
|
+
|
|
105
|
+
/// Only the four corner cells are selectable.
|
|
106
|
+
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
107
|
+
corners: bool,
|
|
108
|
+
},
|
|
77
109
|
Bool {
|
|
78
110
|
key: String,
|
|
79
111
|
default: bool,
|
|
@@ -125,7 +157,6 @@ pub enum ControlSpec {
|
|
|
125
157
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
126
158
|
default: Option<DatetimeFormatType>,
|
|
127
159
|
},
|
|
128
|
-
StringFormat,
|
|
129
160
|
NumberSeriesStyle {
|
|
130
161
|
default: NumberSeriesStyleDefaultConfig,
|
|
131
162
|
},
|
|
@@ -147,6 +178,108 @@ pub enum ControlSpec {
|
|
|
147
178
|
},
|
|
148
179
|
}
|
|
149
180
|
|
|
181
|
+
/// One cell of a 3×3 anchor grid: a corner `top-left`/`top-right`/
|
|
182
|
+
/// `bottom-left`/`bottom-right`, an edge `top`/`left`/`right`/`bottom`, or
|
|
183
|
+
/// `center`.
|
|
184
|
+
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
|
185
|
+
#[serde(rename_all = "kebab-case")]
|
|
186
|
+
pub enum Alignment {
|
|
187
|
+
TopLeft,
|
|
188
|
+
Top,
|
|
189
|
+
TopRight,
|
|
190
|
+
Left,
|
|
191
|
+
Center,
|
|
192
|
+
Right,
|
|
193
|
+
BottomLeft,
|
|
194
|
+
Bottom,
|
|
195
|
+
BottomRight,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
impl Alignment {
|
|
199
|
+
/// Every cell in row-major order.
|
|
200
|
+
pub const ALL: [Alignment; 9] = [
|
|
201
|
+
Alignment::TopLeft,
|
|
202
|
+
Alignment::Top,
|
|
203
|
+
Alignment::TopRight,
|
|
204
|
+
Alignment::Left,
|
|
205
|
+
Alignment::Center,
|
|
206
|
+
Alignment::Right,
|
|
207
|
+
Alignment::BottomLeft,
|
|
208
|
+
Alignment::Bottom,
|
|
209
|
+
Alignment::BottomRight,
|
|
210
|
+
];
|
|
211
|
+
pub const CORNERS: [Alignment; 4] = [
|
|
212
|
+
Alignment::TopLeft,
|
|
213
|
+
Alignment::TopRight,
|
|
214
|
+
Alignment::BottomLeft,
|
|
215
|
+
Alignment::BottomRight,
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
pub fn is_corner(self) -> bool {
|
|
219
|
+
Self::CORNERS.contains(&self)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// The serialized token.
|
|
223
|
+
pub fn as_str(self) -> &'static str {
|
|
224
|
+
match self {
|
|
225
|
+
Alignment::TopLeft => "top-left",
|
|
226
|
+
Alignment::Top => "top",
|
|
227
|
+
Alignment::TopRight => "top-right",
|
|
228
|
+
Alignment::Left => "left",
|
|
229
|
+
Alignment::Center => "center",
|
|
230
|
+
Alignment::Right => "right",
|
|
231
|
+
Alignment::BottomLeft => "bottom-left",
|
|
232
|
+
Alignment::Bottom => "bottom",
|
|
233
|
+
Alignment::BottomRight => "bottom-right",
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
pub fn parse(src: &str) -> Option<Self> {
|
|
238
|
+
Self::ALL.into_iter().find(|x| x.as_str() == src)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// `Top Left`-style text for accessible names.
|
|
242
|
+
pub fn humanized(self) -> String {
|
|
243
|
+
self.as_str()
|
|
244
|
+
.split('-')
|
|
245
|
+
.map(|word| {
|
|
246
|
+
let mut chars = word.chars();
|
|
247
|
+
match chars.next() {
|
|
248
|
+
Some(first) => first.to_uppercase().chain(chars).collect::<String>(),
|
|
249
|
+
None => String::new(),
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
.collect::<Vec<_>>()
|
|
253
|
+
.join(" ")
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// One boolean style toggle of a [`ControlSpec::Font`] control: the
|
|
258
|
+
/// config key it writes and the value that counts as "not set".
|
|
259
|
+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
260
|
+
pub struct FontToggle {
|
|
261
|
+
pub key: String,
|
|
262
|
+
#[serde(default)]
|
|
263
|
+
pub default: bool,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// The size input of a [`ControlSpec::Font`] control: a number (CSS px)
|
|
267
|
+
/// under its own key, elided from serialized configs at `default`.
|
|
268
|
+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
269
|
+
pub struct FontSize {
|
|
270
|
+
pub key: String,
|
|
271
|
+
pub default: f64,
|
|
272
|
+
|
|
273
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
274
|
+
pub min: Option<f64>,
|
|
275
|
+
|
|
276
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
277
|
+
pub max: Option<f64>,
|
|
278
|
+
|
|
279
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
280
|
+
pub step: Option<f64>,
|
|
281
|
+
}
|
|
282
|
+
|
|
150
283
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
151
284
|
pub struct EnumVariant {
|
|
152
285
|
pub value: String,
|
|
@@ -182,9 +315,7 @@ impl ColumnConfigSchema {
|
|
|
182
315
|
fn is_format(spec: &ControlSpec) -> bool {
|
|
183
316
|
matches!(
|
|
184
317
|
spec,
|
|
185
|
-
ControlSpec::NumberFormat { .. }
|
|
186
|
-
| ControlSpec::DatetimeFormat { .. }
|
|
187
|
-
| ControlSpec::StringFormat
|
|
318
|
+
ControlSpec::NumberFormat { .. } | ControlSpec::DatetimeFormat { .. }
|
|
188
319
|
)
|
|
189
320
|
}
|
|
190
321
|
|
|
@@ -230,6 +361,18 @@ impl ColumnConfigSchema {
|
|
|
230
361
|
canonicalize_specs(fields);
|
|
231
362
|
return !fields.is_empty();
|
|
232
363
|
},
|
|
364
|
+
ControlSpec::Alignment {
|
|
365
|
+
key,
|
|
366
|
+
default: Some(default),
|
|
367
|
+
corners: true,
|
|
368
|
+
} if !default.is_corner() => {
|
|
369
|
+
tracing::error!(
|
|
370
|
+
"Dropping `{key}` — default `{}` is not a corner",
|
|
371
|
+
default.as_str()
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return false;
|
|
375
|
+
},
|
|
233
376
|
ControlSpec::Color { key, default } => (CssKind::Color, key, default),
|
|
234
377
|
ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
|
|
235
378
|
ControlSpec::GradientStops { key, default, .. } => {
|
|
@@ -255,6 +398,16 @@ impl ColumnConfigSchema {
|
|
|
255
398
|
self
|
|
256
399
|
}
|
|
257
400
|
|
|
401
|
+
/// The variants of the [`ControlSpec::Enum`] owning `key`, if any.
|
|
402
|
+
pub fn enum_variants_of(&self, key: &str) -> Option<&[EnumVariant]> {
|
|
403
|
+
self.leaf_fields().into_iter().find_map(|spec| match spec {
|
|
404
|
+
ControlSpec::Enum {
|
|
405
|
+
key: k, variants, ..
|
|
406
|
+
} if k == key => Some(variants.as_slice()),
|
|
407
|
+
_ => None,
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
258
411
|
/// The CSS kind of the control owning `key`, if it is CSS-valued.
|
|
259
412
|
pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
|
|
260
413
|
self.leaf_fields().into_iter().find_map(|spec| match spec {
|
|
@@ -275,12 +428,27 @@ impl ControlSpec {
|
|
|
275
428
|
pub fn serialized_keys(&self) -> Vec<&str> {
|
|
276
429
|
match self {
|
|
277
430
|
ControlSpec::DatetimeFormat { .. } => vec!["date_format"],
|
|
278
|
-
ControlSpec::
|
|
431
|
+
ControlSpec::Font {
|
|
432
|
+
key,
|
|
433
|
+
size,
|
|
434
|
+
bold,
|
|
435
|
+
italic,
|
|
436
|
+
..
|
|
437
|
+
} => [
|
|
438
|
+
Some(key.as_str()),
|
|
439
|
+
size.as_ref().map(|s| s.key.as_str()),
|
|
440
|
+
bold.as_ref().map(|t| t.key.as_str()),
|
|
441
|
+
italic.as_ref().map(|t| t.key.as_str()),
|
|
442
|
+
]
|
|
443
|
+
.into_iter()
|
|
444
|
+
.flatten()
|
|
445
|
+
.collect(),
|
|
279
446
|
ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
|
|
280
447
|
ControlSpec::Symbols { .. } => vec!["symbols"],
|
|
281
448
|
ControlSpec::NumberFormat { .. } => vec!["number_format"],
|
|
282
449
|
ControlSpec::AggregateDepth => vec!["aggregate_depth"],
|
|
283
450
|
ControlSpec::Enum { key, .. }
|
|
451
|
+
| ControlSpec::Alignment { key, .. }
|
|
284
452
|
| ControlSpec::Bool { key, .. }
|
|
285
453
|
| ControlSpec::Number { key, .. }
|
|
286
454
|
| ControlSpec::String { key, .. }
|
|
@@ -396,13 +564,46 @@ mod tests {
|
|
|
396
564
|
assert_eq!(flat.css_kind_of("stack"), grouped.css_kind_of("stack"));
|
|
397
565
|
}
|
|
398
566
|
|
|
567
|
+
#[test]
|
|
568
|
+
fn enum_variants_see_through_groups() {
|
|
569
|
+
let mode = ControlSpec::Enum {
|
|
570
|
+
key: "fg_mode".to_owned(),
|
|
571
|
+
default: "color".to_owned(),
|
|
572
|
+
variants: vec![
|
|
573
|
+
EnumVariant {
|
|
574
|
+
value: "disabled".to_owned(),
|
|
575
|
+
label: None,
|
|
576
|
+
},
|
|
577
|
+
EnumVariant {
|
|
578
|
+
value: "color".to_owned(),
|
|
579
|
+
label: None,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
let grouped = ColumnConfigSchema {
|
|
585
|
+
fields: vec![group("color", vec![mode, flag("flag")])],
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
let values: Vec<&str> = grouped
|
|
589
|
+
.enum_variants_of("fg_mode")
|
|
590
|
+
.unwrap()
|
|
591
|
+
.iter()
|
|
592
|
+
.map(|v| v.value.as_str())
|
|
593
|
+
.collect();
|
|
594
|
+
|
|
595
|
+
assert_eq!(values, vec!["disabled", "color"]);
|
|
596
|
+
assert!(grouped.enum_variants_of("flag").is_none());
|
|
597
|
+
assert!(grouped.enum_variants_of("missing").is_none());
|
|
598
|
+
}
|
|
599
|
+
|
|
399
600
|
#[test]
|
|
400
601
|
fn format_controls_group_and_merge() {
|
|
401
602
|
let schema = ColumnConfigSchema {
|
|
402
603
|
fields: vec![
|
|
403
604
|
ControlSpec::NumberFormat { default: None },
|
|
404
605
|
flag("flag"),
|
|
405
|
-
ControlSpec::
|
|
606
|
+
ControlSpec::DatetimeFormat { default: None },
|
|
406
607
|
],
|
|
407
608
|
}
|
|
408
609
|
.group_format_controls();
|
|
@@ -414,19 +615,45 @@ mod tests {
|
|
|
414
615
|
|
|
415
616
|
assert_eq!(key, "format");
|
|
416
617
|
assert!(matches!(fields[0], ControlSpec::NumberFormat { .. }));
|
|
417
|
-
assert!(matches!(fields[1], ControlSpec::
|
|
618
|
+
assert!(matches!(fields[1], ControlSpec::DatetimeFormat { .. }));
|
|
418
619
|
assert!(matches!(&schema.fields[1], ControlSpec::Bool { .. }));
|
|
419
620
|
|
|
420
621
|
assert_eq!(
|
|
421
622
|
schema.active_keys(),
|
|
422
623
|
HashSet::from([
|
|
423
624
|
"number_format".to_owned(),
|
|
424
|
-
"
|
|
625
|
+
"date_format".to_owned(),
|
|
425
626
|
"flag".to_owned()
|
|
426
627
|
])
|
|
427
628
|
);
|
|
428
629
|
}
|
|
429
630
|
|
|
631
|
+
#[test]
|
|
632
|
+
fn font_owns_its_size_and_toggle_keys() {
|
|
633
|
+
let spec = ControlSpec::Font {
|
|
634
|
+
key: "font_family".to_owned(),
|
|
635
|
+
default: "inherit".to_owned(),
|
|
636
|
+
size: Some(FontSize {
|
|
637
|
+
key: "font_size".to_owned(),
|
|
638
|
+
default: 12.0,
|
|
639
|
+
min: None,
|
|
640
|
+
max: None,
|
|
641
|
+
step: None,
|
|
642
|
+
}),
|
|
643
|
+
bold: Some(FontToggle {
|
|
644
|
+
key: "bold".to_owned(),
|
|
645
|
+
default: false,
|
|
646
|
+
}),
|
|
647
|
+
italic: None,
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
assert_eq!(spec.serialized_keys(), vec![
|
|
651
|
+
"font_family",
|
|
652
|
+
"font_size",
|
|
653
|
+
"bold"
|
|
654
|
+
]);
|
|
655
|
+
}
|
|
656
|
+
|
|
430
657
|
#[test]
|
|
431
658
|
fn format_grouping_recurses_but_never_double_wraps() {
|
|
432
659
|
let schema = ColumnConfigSchema {
|
|
@@ -536,6 +763,83 @@ mod tests {
|
|
|
536
763
|
);
|
|
537
764
|
}
|
|
538
765
|
|
|
766
|
+
#[test]
|
|
767
|
+
fn alignment_tokens_round_trip() {
|
|
768
|
+
for align in Alignment::ALL {
|
|
769
|
+
assert_eq!(Alignment::parse(align.as_str()), Some(align));
|
|
770
|
+
assert_eq!(serde_json::to_value(align).unwrap(), json!(align.as_str()));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for bad in ["middle", "left-top", "top-center", "middle-left", ""] {
|
|
774
|
+
assert_eq!(Alignment::parse(bad), None, "{bad}");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
assert_eq!(Alignment::ALL.iter().filter(|x| x.is_corner()).count(), 4);
|
|
778
|
+
assert_eq!(Alignment::TopLeft.humanized(), "Top Left");
|
|
779
|
+
assert_eq!(Alignment::Center.humanized(), "Center");
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
#[test]
|
|
783
|
+
fn alignment_deserializes_with_optional_default_and_corners() {
|
|
784
|
+
let schema: ColumnConfigSchema = serde_json::from_value(json!({
|
|
785
|
+
"fields": [
|
|
786
|
+
{ "kind": "Alignment", "key": "align" },
|
|
787
|
+
{ "kind": "Alignment", "key": "legend_anchor", "default": "top-right", "corners": true }
|
|
788
|
+
]
|
|
789
|
+
}))
|
|
790
|
+
.unwrap();
|
|
791
|
+
|
|
792
|
+
assert!(matches!(&schema.fields[0], ControlSpec::Alignment {
|
|
793
|
+
default: None,
|
|
794
|
+
corners: false,
|
|
795
|
+
..
|
|
796
|
+
}));
|
|
797
|
+
assert!(matches!(&schema.fields[1], ControlSpec::Alignment {
|
|
798
|
+
default: Some(Alignment::TopRight),
|
|
799
|
+
corners: true,
|
|
800
|
+
..
|
|
801
|
+
}));
|
|
802
|
+
assert_eq!(
|
|
803
|
+
schema.active_keys(),
|
|
804
|
+
HashSet::from(["align".to_owned(), "legend_anchor".to_owned()])
|
|
805
|
+
);
|
|
806
|
+
assert!(
|
|
807
|
+
serde_json::from_value::<ColumnConfigSchema>(json!({
|
|
808
|
+
"fields": [{ "kind": "Alignment", "key": "x", "default": "middle" }]
|
|
809
|
+
}))
|
|
810
|
+
.is_err()
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
#[test]
|
|
815
|
+
fn canonicalize_defaults_drops_non_corner_default_of_corners_field() {
|
|
816
|
+
let schema = ColumnConfigSchema {
|
|
817
|
+
fields: vec![
|
|
818
|
+
ControlSpec::Alignment {
|
|
819
|
+
key: "bad".to_owned(),
|
|
820
|
+
default: Some(Alignment::Top),
|
|
821
|
+
corners: true,
|
|
822
|
+
},
|
|
823
|
+
ControlSpec::Alignment {
|
|
824
|
+
key: "ok".to_owned(),
|
|
825
|
+
default: Some(Alignment::Top),
|
|
826
|
+
corners: false,
|
|
827
|
+
},
|
|
828
|
+
ControlSpec::Alignment {
|
|
829
|
+
key: "unset".to_owned(),
|
|
830
|
+
default: None,
|
|
831
|
+
corners: true,
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
}
|
|
835
|
+
.canonicalize_defaults();
|
|
836
|
+
|
|
837
|
+
assert_eq!(
|
|
838
|
+
schema.active_keys(),
|
|
839
|
+
HashSet::from(["ok".to_owned(), "unset".to_owned()])
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
539
843
|
#[test]
|
|
540
844
|
fn canonicalize_defaults_recurses_and_drops_empty_groups() {
|
|
541
845
|
let schema = ColumnConfigSchema {
|
|
@@ -10,13 +10,11 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
mod color_mode;
|
|
14
13
|
mod custom;
|
|
15
14
|
mod custom_format;
|
|
16
15
|
mod simple;
|
|
17
16
|
mod simple_format;
|
|
18
17
|
|
|
19
|
-
pub use color_mode::*;
|
|
20
18
|
pub use custom::*;
|
|
21
19
|
pub use custom_format::*;
|
|
22
20
|
use serde::{Deserialize, Serialize};
|
|
@@ -65,13 +63,4 @@ pub struct DatetimeColumnStyleConfig {
|
|
|
65
63
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
66
64
|
#[ts(optional, as = "Option<_>")]
|
|
67
65
|
pub date_format: Option<DatetimeFormatType>,
|
|
68
|
-
|
|
69
|
-
#[serde(default)]
|
|
70
|
-
#[serde(skip_serializing_if = "DatetimeColorMode::is_none")]
|
|
71
|
-
pub datetime_color_mode: DatetimeColorMode,
|
|
72
|
-
|
|
73
|
-
#[serde(default)]
|
|
74
|
-
#[serde(skip_serializing_if = "Option::is_none")]
|
|
75
|
-
#[ts(skip)]
|
|
76
|
-
pub color: Option<String>,
|
|
77
66
|
}
|