@perspective-dev/viewer 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/docs/perspective-docs.json +1 -1
  17. package/dist/esm/perspective-viewer.d.ts +1 -0
  18. package/dist/esm/perspective-viewer.inline.js +9 -9
  19. package/dist/esm/perspective-viewer.inline.js.map +2 -2
  20. package/dist/esm/perspective-viewer.js +4 -4
  21. package/dist/esm/perspective-viewer.js.map +2 -2
  22. package/dist/esm/plugin.d.ts +10 -0
  23. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
  24. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  25. package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
  26. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  27. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
  28. package/dist/wasm/perspective-viewer.d.ts +43 -36
  29. package/dist/wasm/perspective-viewer.js +194 -136
  30. package/dist/wasm/perspective-viewer.wasm +0 -0
  31. package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
  32. package/package.json +1 -1
  33. package/src/css/column-settings-panel.css +9 -5
  34. package/src/css/column-style.css +209 -31
  35. package/src/css/dom/select.css +2 -1
  36. package/src/css/form/code-editor.css +4 -4
  37. package/src/rust/agent/tools.rs +48 -17
  38. package/src/rust/components/column_selector/active_column.rs +2 -2
  39. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  40. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  41. package/src/rust/components/column_selector/config_selector.rs +8 -0
  42. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  43. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  44. package/src/rust/components/column_selector.rs +48 -4
  45. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  46. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  47. package/src/rust/components/column_settings_sidebar.rs +88 -69
  48. package/src/rust/components/editable_header.rs +85 -144
  49. package/src/rust/components/expression_editor.rs +9 -16
  50. package/src/rust/components/form/code_editor.rs +16 -6
  51. package/src/rust/components/form/debug.rs +1 -0
  52. package/src/rust/components/form/mod.rs +3 -1
  53. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  54. package/src/rust/components/form/named_value_picker.rs +86 -0
  55. package/src/rust/components/form/palette_selector.rs +440 -0
  56. package/src/rust/components/main_panel/msg.rs +2 -2
  57. package/src/rust/components/main_panel/reconcile.rs +4 -4
  58. package/src/rust/components/plugin_tab.rs +4 -28
  59. package/src/rust/components/settings_panel.rs +25 -20
  60. package/src/rust/components/viewer/msg.rs +46 -7
  61. package/src/rust/components/viewer/panels.rs +11 -9
  62. package/src/rust/components/viewer/render.rs +8 -3
  63. package/src/rust/components/viewer/settings.rs +163 -23
  64. package/src/rust/components/viewer/snapshots.rs +46 -21
  65. package/src/rust/components/viewer/wiring.rs +14 -25
  66. package/src/rust/components/viewer.rs +12 -8
  67. package/src/rust/config/column_config_schema.rs +100 -41
  68. package/src/rust/config/css_scale.rs +882 -0
  69. package/src/rust/config/mod.rs +2 -0
  70. package/src/rust/config/options.rs +10 -0
  71. package/src/rust/config/viewer_config.rs +49 -55
  72. package/src/rust/config/workspace_config.rs +132 -5
  73. package/src/rust/custom_elements/viewer.rs +214 -155
  74. package/src/rust/custom_events.rs +14 -1
  75. package/src/rust/js/regular_layout.rs +3 -3
  76. package/src/rust/lib.rs +2 -0
  77. package/src/rust/presentation/column_locator.rs +19 -5
  78. package/src/rust/presentation.rs +192 -36
  79. package/src/rust/queries/column_locator.rs +74 -44
  80. package/src/rust/queries/get_viewer_config.rs +0 -3
  81. package/src/rust/queries/mod.rs +2 -0
  82. package/src/rust/queries/palette_set.rs +82 -0
  83. package/src/rust/queries/plugin_column_styles.rs +6 -2
  84. package/src/rust/renderer/dispatch.rs +16 -4
  85. package/src/rust/renderer/plugin_config.rs +309 -47
  86. package/src/rust/renderer.rs +15 -42
  87. package/src/rust/session/column_defaults_update.rs +15 -8
  88. package/src/rust/session/metadata.rs +5 -1
  89. package/src/rust/session/props.rs +1 -0
  90. package/src/rust/session.rs +9 -6
  91. package/src/rust/tasks/create_panel.rs +44 -57
  92. package/src/rust/tasks/edit_expression.rs +19 -23
  93. package/src/rust/tasks/edit_window.rs +8 -10
  94. package/src/rust/tasks/eject.rs +4 -1
  95. package/src/rust/tasks/pipeline.rs +2 -2
  96. package/src/rust/tasks/presize_panels.rs +44 -5
  97. package/src/rust/tasks/reset_all.rs +4 -4
  98. package/src/rust/tasks/restore_and_render.rs +17 -44
  99. package/src/rust/tasks/restore_panel.rs +11 -13
  100. package/src/rust/tasks/set_edit_mode.rs +3 -1
  101. package/src/rust/tasks/sync_update_panels.rs +11 -1
  102. package/src/rust/tasks/update_theme.rs +23 -18
  103. package/src/rust/utils/css_vars.rs +26 -0
  104. package/src/rust/utils/mod.rs +2 -0
  105. package/src/rust/utils/spawn.rs +1 -1
  106. package/src/rust/workspace.rs +435 -272
  107. package/src/svg/lock.svg +7 -0
  108. package/src/themes/botanical.css +10 -0
  109. package/src/themes/defaults.css +12 -0
  110. package/src/themes/dracula.css +17 -0
  111. package/src/themes/gruvbox-dark.css +20 -0
  112. package/src/themes/gruvbox.css +10 -0
  113. package/src/themes/icons.css +1 -0
  114. package/src/themes/monokai.css +22 -0
  115. package/src/themes/phosphor.css +5 -0
  116. package/src/themes/pro-dark.css +10 -0
  117. package/src/themes/solarized-dark.css +7 -0
  118. package/src/themes/solarized.css +7 -0
  119. package/src/themes/vaporwave.css +20 -0
  120. package/src/ts/perspective-viewer.ts +1 -0
  121. package/src/ts/plugin.ts +14 -0
  122. package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
  123. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  124. package/src/ts/ts-rs/ViewerConfig.ts +1 -1
  125. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  126. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
  127. package/src/rust/components/form/color_range_selector.rs +0 -137
  128. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  129. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  130. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  131. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  132. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  133. /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -0,0 +1,440 @@
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 wasm_bindgen::JsCast;
14
+ use web_sys::*;
15
+ use yew::prelude::*;
16
+
17
+ /// The widget for [`crate::config::ControlSpec::Palette`]: an ordered row
18
+ /// of discrete color swatches with per-swatch edit/remove, pointer-drag
19
+ /// reordering, and a trailing add tile.
20
+ #[derive(Properties, PartialEq)]
21
+ pub struct PaletteProps {
22
+ pub values: Vec<String>,
23
+ pub on_change: Callback<Vec<String>>,
24
+ pub on_reset: Callback<()>,
25
+ pub is_modified: bool,
26
+
27
+ #[prop_or_default]
28
+ pub max: Option<usize>,
29
+
30
+ #[prop_or_default]
31
+ pub title: Option<String>,
32
+ }
33
+
34
+ const DRAG_THRESHOLD_PX: f64 = 4.0;
35
+
36
+ #[derive(Clone, Copy)]
37
+ struct Press {
38
+ pointer_id: i32,
39
+ x: i32,
40
+ y: i32,
41
+ from: usize,
42
+ }
43
+
44
+ fn event_index(target: &Element) -> Option<usize> {
45
+ target
46
+ .get_attribute("data-index")
47
+ .and_then(|x| x.parse::<usize>().ok())
48
+ }
49
+
50
+ fn insertion_index(centers: &[(f64, f64)], x: f64, y: f64) -> usize {
51
+ let nearest = centers
52
+ .iter()
53
+ .enumerate()
54
+ .min_by(|(_, a), (_, b)| {
55
+ let da = (a.0 - x).hypot(a.1 - y);
56
+ let db = (b.0 - x).hypot(b.1 - y);
57
+ da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
58
+ })
59
+ .map(|(index, _)| index);
60
+
61
+ match nearest {
62
+ Some(index) if x < centers[index].0 => index,
63
+ Some(index) => index + 1,
64
+ None => 0,
65
+ }
66
+ }
67
+
68
+ fn reordered<T: Clone>(values: &[T], from: usize, insert: usize) -> Option<Vec<T>> {
69
+ if from >= values.len() || insert > values.len() || insert == from || insert == from + 1 {
70
+ return None;
71
+ }
72
+
73
+ let mut next = values.to_vec();
74
+ let color = next.remove(from);
75
+ let at = if insert > from { insert - 1 } else { insert };
76
+ next.insert(at, color);
77
+ Some(next)
78
+ }
79
+
80
+ fn slot_from_display(slot: usize, shown_at: usize, from: usize) -> usize {
81
+ let shown = if slot > shown_at { slot - 1 } else { slot };
82
+ match shown.cmp(&from) {
83
+ std::cmp::Ordering::Less => shown,
84
+ std::cmp::Ordering::Greater => shown + 1,
85
+ std::cmp::Ordering::Equal => from,
86
+ }
87
+ }
88
+
89
+ fn swatch_centers(container: &Element) -> Vec<(f64, f64, usize)> {
90
+ let Ok(nodes) = container.query_selector_all(".palette-swatch") else {
91
+ return vec![];
92
+ };
93
+
94
+ (0..nodes.length())
95
+ .filter_map(|i| nodes.get(i))
96
+ .filter_map(|node| node.dyn_into::<Element>().ok())
97
+ .filter_map(|el| {
98
+ let index = event_index(&el)?;
99
+ let rect = el.get_bounding_client_rect();
100
+ Some((
101
+ rect.left() + rect.width() / 2.0,
102
+ rect.top() + rect.height() / 2.0,
103
+ index,
104
+ ))
105
+ })
106
+ .collect()
107
+ }
108
+
109
+ #[function_component(PaletteSelector)]
110
+ pub fn palette_selector(props: &PaletteProps) -> Html {
111
+ let oninput = use_callback(
112
+ (props.values.clone(), props.on_change.clone()),
113
+ |event: InputEvent, (values, on_change)| {
114
+ let input = event.target().unwrap().unchecked_into::<HtmlInputElement>();
115
+
116
+ let Some(index) = event_index(&input) else {
117
+ return;
118
+ };
119
+
120
+ let mut next = values.clone();
121
+ let Some(slot) = next.get_mut(index) else {
122
+ return;
123
+ };
124
+
125
+ *slot = input.value();
126
+ on_change.emit(next);
127
+ },
128
+ );
129
+
130
+ let on_remove = use_callback(
131
+ (props.values.clone(), props.on_change.clone()),
132
+ |event: MouseEvent, (values, on_change)| {
133
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
134
+ return;
135
+ };
136
+
137
+ let Some(index) = event_index(&target) else {
138
+ return;
139
+ };
140
+
141
+ if values.len() > 1 && index < values.len() {
142
+ let mut next = values.clone();
143
+ next.remove(index);
144
+ on_change.emit(next);
145
+ }
146
+ },
147
+ );
148
+
149
+ let swatches_ref = use_node_ref();
150
+ let open_index = use_mut_ref(|| None::<usize>);
151
+ let on_add = use_callback(
152
+ (
153
+ props.values.clone(),
154
+ props.on_change.clone(),
155
+ props.max,
156
+ open_index.clone(),
157
+ ),
158
+ |_: MouseEvent, (values, on_change, max, open_index)| {
159
+ if max.map(|max| values.len() < max).unwrap_or(true)
160
+ && let Some(last) = values.last()
161
+ {
162
+ let mut next = values.clone();
163
+ next.push(last.clone());
164
+ *open_index.borrow_mut() = Some(values.len());
165
+ on_change.emit(next);
166
+ }
167
+ },
168
+ );
169
+
170
+ {
171
+ let swatches_ref = swatches_ref.clone();
172
+ let open_index = open_index.clone();
173
+ use_effect(move || {
174
+ let Some(index) = *open_index.borrow() else {
175
+ return;
176
+ };
177
+
178
+ let input = swatches_ref
179
+ .cast::<Element>()
180
+ .and_then(|el| {
181
+ el.query_selector(&format!("input[data-index=\"{index}\"]"))
182
+ .ok()
183
+ .flatten()
184
+ })
185
+ .and_then(|el| el.dyn_into::<HtmlInputElement>().ok());
186
+
187
+ if let Some(input) = input {
188
+ *open_index.borrow_mut() = None;
189
+ let _ = input.focus();
190
+ if input.show_picker().is_err() {
191
+ input.click();
192
+ }
193
+ }
194
+ });
195
+ }
196
+
197
+ let press = use_mut_ref(|| None::<Press>);
198
+ let drag = use_state(|| None::<(usize, usize)>);
199
+ let suppress_click = use_mut_ref(|| false);
200
+
201
+ let on_pointerdown = {
202
+ let press = press.clone();
203
+ let suppress_click = suppress_click.clone();
204
+ Callback::from(move |event: PointerEvent| {
205
+ if event.button() != 0 {
206
+ return;
207
+ }
208
+
209
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
210
+ return;
211
+ };
212
+
213
+ if target.class_list().contains("palette-swatch-remove") {
214
+ return;
215
+ }
216
+
217
+ let Some(swatch) = target.closest(".palette-swatch").ok().flatten() else {
218
+ return;
219
+ };
220
+
221
+ let Some(from) = event_index(&swatch) else {
222
+ return;
223
+ };
224
+
225
+ *suppress_click.borrow_mut() = false;
226
+ *press.borrow_mut() = Some(Press {
227
+ pointer_id: event.pointer_id(),
228
+ x: event.client_x(),
229
+ y: event.client_y(),
230
+ from,
231
+ });
232
+ })
233
+ };
234
+
235
+ let on_pointermove = {
236
+ let press = press.clone();
237
+ let drag = drag.clone();
238
+ let swatches_ref = swatches_ref.clone();
239
+ let suppress_click = suppress_click.clone();
240
+ Callback::from(move |event: PointerEvent| {
241
+ let Some(pressed) = *press.borrow() else {
242
+ return;
243
+ };
244
+
245
+ if pressed.pointer_id != event.pointer_id() {
246
+ return;
247
+ }
248
+
249
+ let Some(container) = swatches_ref.cast::<Element>() else {
250
+ return;
251
+ };
252
+
253
+ if drag.is_none() {
254
+ let dx = (event.client_x() - pressed.x) as f64;
255
+ let dy = (event.client_y() - pressed.y) as f64;
256
+ if dx.hypot(dy) < DRAG_THRESHOLD_PX {
257
+ return;
258
+ }
259
+
260
+ let _ = container.set_pointer_capture(pressed.pointer_id);
261
+ *suppress_click.borrow_mut() = true;
262
+ }
263
+
264
+ let shown = swatch_centers(&container);
265
+ let Some(shown_at) = shown
266
+ .iter()
267
+ .position(|(_, _, index)| *index == pressed.from)
268
+ else {
269
+ return;
270
+ };
271
+
272
+ let centers = shown.iter().map(|(x, y, _)| (*x, *y)).collect::<Vec<_>>();
273
+ let slot = insertion_index(&centers, event.client_x() as f64, event.client_y() as f64);
274
+ let insert = slot_from_display(slot, shown_at, pressed.from);
275
+ if *drag != Some((pressed.from, insert)) {
276
+ drag.set(Some((pressed.from, insert)));
277
+ }
278
+ })
279
+ };
280
+
281
+ let on_pointerup = {
282
+ let press = press.clone();
283
+ let drag = drag.clone();
284
+ let values = props.values.clone();
285
+ let on_change = props.on_change.clone();
286
+ Callback::from(move |event: PointerEvent| {
287
+ let pressed = press
288
+ .borrow()
289
+ .is_some_and(|p| p.pointer_id == event.pointer_id());
290
+
291
+ if !pressed {
292
+ return;
293
+ }
294
+
295
+ *press.borrow_mut() = None;
296
+ if let Some((from, insert)) = *drag {
297
+ drag.set(None);
298
+ if let Some(next) = reordered(&values, from, insert) {
299
+ on_change.emit(next);
300
+ }
301
+ }
302
+ })
303
+ };
304
+
305
+ let on_pointercancel = {
306
+ let press = press.clone();
307
+ let drag = drag.clone();
308
+ Callback::from(move |_: PointerEvent| {
309
+ *press.borrow_mut() = None;
310
+ if drag.is_some() {
311
+ drag.set(None);
312
+ }
313
+ })
314
+ };
315
+
316
+ let on_swatch_click = {
317
+ let suppress_click = suppress_click.clone();
318
+ Callback::from(move |event: MouseEvent| {
319
+ if std::mem::replace(&mut *suppress_click.borrow_mut(), false) {
320
+ event.prevent_default();
321
+ }
322
+ })
323
+ };
324
+
325
+ let on_reset = use_callback(props.on_reset.clone(), |_: MouseEvent, on_reset| {
326
+ on_reset.emit(())
327
+ });
328
+
329
+ let drag_from = drag.map(|(from, _)| from);
330
+ let identity = (0..props.values.len()).collect::<Vec<_>>();
331
+ let display = (*drag)
332
+ .and_then(|(from, insert)| reordered(&identity, from, insert))
333
+ .unwrap_or(identity);
334
+
335
+ let can_add = props
336
+ .max
337
+ .map(|max| props.values.len() < max)
338
+ .unwrap_or(true);
339
+ let can_remove = props.values.len() > 1;
340
+
341
+ html! {
342
+ <>
343
+ <label id={props.title.as_deref().unwrap_or("palette-label").to_owned()} />
344
+ <div class="color-gradient-container palette-selector">
345
+ <div
346
+ class={classes!("palette-swatches", drag.is_some().then_some("reordering"))}
347
+ ref={swatches_ref}
348
+ onpointerdown={on_pointerdown}
349
+ onpointermove={on_pointermove}
350
+ onpointerup={on_pointerup}
351
+ onpointercancel={on_pointercancel}
352
+ >
353
+ { for display.iter().map(|&index| html! {
354
+ <div
355
+ class={classes!(
356
+ "palette-swatch",
357
+ (drag_from == Some(index)).then_some("dragging"),
358
+ )}
359
+ key={index.to_string()}
360
+ data-index={index.to_string()}
361
+ >
362
+ <input
363
+ class="parameter"
364
+ type="color"
365
+ value={props.values[index].clone()}
366
+ data-index={index.to_string()}
367
+ oninput={oninput.clone()}
368
+ onclick={on_swatch_click.clone()}
369
+ />
370
+ if can_remove {
371
+ <span
372
+ class="palette-swatch-remove"
373
+ data-index={index.to_string()}
374
+ onclick={on_remove.clone()}
375
+ />
376
+ }
377
+ </div>
378
+ }) }
379
+ if can_add { <span class="palette-add" onclick={on_add} /> }
380
+ </div>
381
+ if props.is_modified {
382
+ <span class="reset-default-style" onclick={on_reset} />
383
+ } else {
384
+ <span class="reset-default-style-disabled" />
385
+ }
386
+ </div>
387
+ </>
388
+ }
389
+ }
390
+
391
+ #[cfg(test)]
392
+ mod tests {
393
+ use super::*;
394
+
395
+ fn v(items: &[&str]) -> Vec<String> {
396
+ items.iter().map(|x| x.to_string()).collect()
397
+ }
398
+
399
+ #[test]
400
+ fn insertion_index_splits_at_centers_and_resolves_rows() {
401
+ let centers = [(12.0, 12.0), (40.0, 12.0), (68.0, 12.0), (12.0, 40.0)];
402
+ assert_eq!(insertion_index(&centers, 0.0, 12.0), 0);
403
+ assert_eq!(insertion_index(&centers, 13.0, 12.0), 1);
404
+ assert_eq!(insertion_index(&centers, 39.0, 12.0), 1);
405
+ assert_eq!(insertion_index(&centers, 41.0, 12.0), 2);
406
+ assert_eq!(insertion_index(&centers, 90.0, 12.0), 3);
407
+ assert_eq!(insertion_index(&centers, 10.0, 41.0), 3);
408
+ assert_eq!(insertion_index(&centers, 30.0, 41.0), 4);
409
+ assert_eq!(insertion_index(&[], 5.0, 5.0), 0);
410
+ }
411
+
412
+ #[test]
413
+ fn slot_from_display_maps_previewed_slots_back_to_original_order() {
414
+ assert_eq!(slot_from_display(3, 2, 0), 3);
415
+ assert_eq!(slot_from_display(2, 2, 0), 3);
416
+ assert_eq!(slot_from_display(1, 2, 0), 2);
417
+ assert_eq!(slot_from_display(0, 2, 0), 0);
418
+ assert_eq!(slot_from_display(0, 0, 2), 0);
419
+ assert_eq!(slot_from_display(1, 0, 2), 0);
420
+ assert_eq!(slot_from_display(2, 0, 2), 1);
421
+ assert_eq!(slot_from_display(3, 0, 2), 2);
422
+ assert_eq!(slot_from_display(4, 0, 2), 4);
423
+ assert_eq!(slot_from_display(1, 1, 1), 1);
424
+ assert_eq!(slot_from_display(2, 1, 1), 1);
425
+ assert_eq!(slot_from_display(3, 1, 1), 3);
426
+ }
427
+
428
+ #[test]
429
+ fn reordered_moves_and_rejects_no_ops() {
430
+ let values = v(&["a", "b", "c", "d"]);
431
+ assert_eq!(reordered(&values, 0, 4), Some(v(&["b", "c", "d", "a"])));
432
+ assert_eq!(reordered(&values, 3, 0), Some(v(&["d", "a", "b", "c"])));
433
+ assert_eq!(reordered(&values, 0, 2), Some(v(&["b", "a", "c", "d"])));
434
+ assert_eq!(reordered(&values, 2, 1), Some(v(&["a", "c", "b", "d"])));
435
+ assert_eq!(reordered(&values, 1, 1), None);
436
+ assert_eq!(reordered(&values, 1, 2), None);
437
+ assert_eq!(reordered(&values, 4, 0), None);
438
+ assert_eq!(reordered(&values, 0, 5), None);
439
+ }
440
+ }
@@ -16,8 +16,8 @@ use crate::components::panel_menu::PanelCommand;
16
16
  pub enum MainPanelMsg {
17
17
  PointerEvent(web_sys::PointerEvent),
18
18
 
19
- /// The `Workspace` staged-panel set changed (`staged_changed` PubSub —
20
- /// see `Workspace::stage_panel`): re-render so cells/tabs/wrappers
19
+ /// A panel's `PanelPhase` changed (`staged_changed` PubSub — see
20
+ /// `Workspace::insert_panel`/`promote`): re-render so cells/tabs/wrappers
21
21
  /// reflect it and `reconcile` inserts a promoted panel. Subscribed on
22
22
  /// MainPanel's OWN scope — the root's `LayoutChanged` delivery proved
23
23
  /// unreliable from element-API task contexts (promoted panels stranded
@@ -41,7 +41,7 @@ const LAYOUT_PHYSICS: LayoutPhysics = LayoutPhysics {
41
41
 
42
42
  impl MainPanel {
43
43
  /// Size each STAGED panel's hidden wrapper (`.psp-staging` — see
44
- /// `MainPanel::render` and `Workspace::stage_panel`) to its PREDICTED
44
+ /// `MainPanel::render` and `PanelPhase::Staging`) to its PREDICTED
45
45
  /// post-insert cell: an equal share of the layout box's width (the
46
46
  /// reconcile insert splits the root horizontally with equal
47
47
  /// redistribution) minus the frame-chrome fallback the presize sweep
@@ -139,7 +139,7 @@ impl MainPanel {
139
139
  let layout: RegularLayout = el.unchecked_into();
140
140
  let panel_ids = &ctx.props().panel_ids;
141
141
 
142
- // Whole-element restore stages its saved layout tree on the Workspace
142
+ // `restoreWorkspace` stages its saved layout tree on the Workspace
143
143
  // (the model; regular-layout is a slave view). Apply it here, BEFORE
144
144
  // the insert reconcile, and seed `inserted` from its panel names — so
145
145
  // restored panels mount directly at their saved positions in ONE
@@ -165,8 +165,8 @@ impl MainPanel {
165
165
 
166
166
  // STAGED panels are withheld from the layout: their first draw
167
167
  // is completing in the hidden staging wrapper (see
168
- // `Workspace::stage_panel`). The promote re-render clears the
169
- // flag, and this loop then inserts the already-drawn panel.
168
+ // `PanelPhase::Staging`). The promote re-render flips the phase,
169
+ // and this loop then inserts the already-drawn panel.
170
170
  if ctx.props().workspace.is_staged(id) {
171
171
  continue;
172
172
  }
@@ -20,7 +20,7 @@ use perspective_client::config::ViewConfig;
20
20
  use yew::prelude::*;
21
21
 
22
22
  use crate::components::column_settings_sidebar::style_tab::primitive_field::{
23
- BoolField, ColorField, ColorRangeField, EnumField, NumberFieldPrimitive,
23
+ BoolField, ColorField, EnumField, NumberFieldPrimitive,
24
24
  };
25
25
  use crate::config::ControlSpec;
26
26
  use crate::queries::get_plugin_config_schema;
@@ -143,32 +143,6 @@ pub fn PluginTab(props: &PluginTabProps) -> Html {
143
143
  />
144
144
  }
145
145
  },
146
- ControlSpec::ColorRange {
147
- key_pos,
148
- key_neg,
149
- default_pos,
150
- default_neg,
151
- is_gradient,
152
- } => {
153
- let current_pos = raw_config
154
- .get(&key_pos)
155
- .and_then(|v| v.as_str().map(|s| s.to_string()));
156
- let current_neg = raw_config
157
- .get(&key_neg)
158
- .and_then(|v| v.as_str().map(|s| s.to_string()));
159
- html! {
160
- <ColorRangeField
161
- field_key_pos={key_pos}
162
- field_key_neg={key_neg}
163
- {default_pos}
164
- {default_neg}
165
- {current_pos}
166
- {current_neg}
167
- {is_gradient}
168
- on_change={on_change.clone()}
169
- />
170
- }
171
- },
172
146
  ControlSpec::Number {
173
147
  key,
174
148
  default,
@@ -199,7 +173,9 @@ pub fn PluginTab(props: &PluginTabProps) -> Html {
199
173
  | ControlSpec::StringFormat
200
174
  | ControlSpec::Symbols { .. }
201
175
  | ControlSpec::NumberFormat
202
- | ControlSpec::String { .. } => {
176
+ | ControlSpec::String { .. }
177
+ | ControlSpec::Palette { .. }
178
+ | ControlSpec::GradientStops { .. } => {
203
179
  return None;
204
180
  },
205
181
  };
@@ -22,7 +22,8 @@ use super::plugin_tab::PluginTab;
22
22
  use crate::components::containers::sidebar_close_button::SidebarCloseButton;
23
23
  use crate::components::form::debug::DebugPanel;
24
24
  use crate::config::{PluginStaticConfig, PluginUpdate};
25
- use crate::presentation::{ColumnLocator, OpenColumnSettings, Presentation};
25
+ use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings, Presentation};
26
+ use crate::queries::classify_column;
26
27
  use crate::renderer::*;
27
28
  use crate::session::column_defaults_update::*;
28
29
  use crate::session::*;
@@ -34,7 +35,7 @@ use crate::workspace::Workspace;
34
35
  pub struct SettingsPanelProps {
35
36
  pub on_close: Callback<()>,
36
37
  pub on_resize: Rc<PubSub<()>>,
37
- pub on_select_column: Callback<Option<ColumnLocator>>,
38
+ pub on_select_column: Callback<Option<ColumnSettingsTarget>>,
38
39
  pub on_debug: Callback<()>,
39
40
  pub is_debug: bool,
40
41
 
@@ -145,25 +146,28 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
145
146
  } = &props;
146
147
 
147
148
  let selected_column = {
148
- let locator = props.open_column_settings.locator.clone();
149
149
  let config = &props.view_config;
150
- locator.filter(|locator| match locator {
151
- ColumnLocator::Table(_name) => {
152
- locator
153
- .name()
154
- .map(|n| {
155
- config.columns.iter().any(|maybe_col| {
156
- maybe_col.as_ref().map(|col| col == n).unwrap_or_default()
157
- }) || config.group_by.iter().any(|col| col == n)
158
- || config.split_by.iter().any(|col| col == n)
159
- || config.filter.iter().any(|col| col.column() == n)
160
- || config.sort.iter().any(|col| &col.0 == n)
161
- })
162
- .unwrap_or_default()
163
- && props.renderer.can_render_column_styles()
164
- },
165
- _ => true,
166
- })
150
+ props
151
+ .open_column_settings
152
+ .target
153
+ .as_ref()
154
+ .and_then(|target| match target {
155
+ ColumnSettingsTarget::NewExpression => Some(ColumnLocator::NewExpression),
156
+ ColumnSettingsTarget::Column(n) => {
157
+ let locator = classify_column(n, config, &props.metadata)?;
158
+ if !matches!(locator, ColumnLocator::Table(_)) {
159
+ return Some(locator);
160
+ }
161
+
162
+ let used = config.columns.iter().any(|maybe_col| {
163
+ maybe_col.as_ref().map(|col| col == n).unwrap_or_default()
164
+ }) || config.group_by.iter().any(|col| col == n)
165
+ || config.split_by.iter().any(|col| col == n)
166
+ || config.filter.iter().any(|col| col.column() == n)
167
+ || config.sort.iter().any(|col| &col.0 == n);
168
+ (used && props.renderer.can_render_column_styles()).then_some(locator)
169
+ },
170
+ })
167
171
  };
168
172
 
169
173
  let plugin_name = props.plugin_name.clone();
@@ -217,6 +221,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
217
221
 
218
222
  update.set_update_column_defaults(
219
223
  &session_metadata,
224
+ &view_config,
220
225
  &view_config.columns,
221
226
  plugin_config,
222
227
  );