@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
@@ -22,7 +22,9 @@ use wasm_bindgen::JsValue;
22
22
 
23
23
  use crate::components::settings_panel::SelectedTab;
24
24
  use crate::config::*;
25
- use crate::presentation::{ColumnLocator, ColumnSettingsTab, DragDropProps, PresentationProps};
25
+ use crate::presentation::{
26
+ ColumnSettingsTab, ColumnSettingsTarget, DragDropProps, PresentationProps,
27
+ };
26
28
  use crate::renderer::RendererProps;
27
29
  use crate::session::{SessionProps, TableLoadState, ViewStats};
28
30
  use crate::utils::Completion;
@@ -48,17 +50,19 @@ pub enum PerspectiveViewerMsg {
48
50
  ColumnSettingsPanelSizeUpdate(Option<i32>),
49
51
  ColumnSettingsPanelAutoWidth(f64),
50
52
  ToggleColumnSettingsPin,
53
+ ToggleColumnSettingsPinComplete(Sender<()>),
51
54
  ColumnSettingsTabChanged(ColumnSettingsTab),
52
55
  OpenColumnSettings {
53
- locator: Option<ColumnLocator>,
56
+ target: Option<ColumnSettingsTarget>,
54
57
  sender: Option<Sender<()>>,
55
58
  toggle: bool,
56
59
  },
57
60
  PreloadFontsUpdate,
58
61
 
59
62
  /// Element-level reset (the public `reset()` API): reset EVERY panel and
60
- /// clear the cross-filter overlay, symmetric with whole-element
61
- /// `save`/`restore`. The `bool` also clears expressions/column settings.
63
+ /// clear the cross-filter overlay, symmetric with
64
+ /// `saveWorkspace`/`restoreWorkspace`. The `bool` also clears
65
+ /// expressions/column settings.
62
66
  Reset(bool, Option<Completion>),
63
67
 
64
68
  /// Reset ONLY the named panel — or the active panel when `None` — to its
@@ -86,10 +90,10 @@ pub enum PerspectiveViewerMsg {
86
90
  /// I6) — carrying any teardown error, which was previously dropped.
87
91
  ClosePanel(String, Option<Completion>),
88
92
 
89
- /// Whole-element `restore` finished replacing the panel set in the
93
+ /// `restoreWorkspace` finished replacing the panel set in the
90
94
  /// `Workspace` (new models inserted, old panels ejected, layout staged):
91
95
  /// activate the named panel, re-subscribe the per-panel wiring, and
92
- /// re-render — the SINGLE visible commit of the whole restore.
96
+ /// re-render — the SINGLE visible commit of the restore.
93
97
  CommitWorkspaceRestore(String),
94
98
 
95
99
  /// Duplicate the named panel: snapshot its config into a new independent
@@ -149,8 +153,34 @@ pub enum PerspectiveViewerMsg {
149
153
  SettingsPanelTabChanged(SelectedTab),
150
154
  SettingsPanelAutoWidth(f64),
151
155
  ToggleDebug,
156
+
157
+ /// The toggle choreography's INTERNAL completion leaf: flip the pane
158
+ /// and resolve on the render commit. Never send this to toggle
159
+ /// settings from outside `settings.rs` — it skips the presize/resize
160
+ /// sweep (the pane's `SplitPanel` emits no `before-resize` and the
161
+ /// host box is unchanged, so nothing else resizes the plugins), which
162
+ /// leaves canvas plugins CSS-stretched at their old backing size. API
163
+ /// entry points send [`Self::ToggleSettingsInit`].
152
164
  ToggleSettingsComplete(SettingsUpdate, Sender<()>),
153
- ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
165
+
166
+ /// Toggle (or force) the settings pane with the FULL choreography:
167
+ /// presize every visible plugin to its post-toggle box, commit the
168
+ /// pane, then the exactness-finalizer resize. The `Sender` resolves
169
+ /// after the sweep. The one settings-toggle entry point for both the
170
+ /// toolbar and the element API (`toggleConfig`, `restore({settings})`,
171
+ /// `restoreWorkspace`).
172
+ ///
173
+ /// The `bool` is `announce`: `true` when this toggle is the SOLE
174
+ /// carrier of the config change (a user gesture — toolbar,
175
+ /// `toggleConfig`), which emits `toggle-settings` + one
176
+ /// `perspective-config-update`; `false` for the `restore` family,
177
+ /// whose own view-config commit dispatch announces the settings field
178
+ /// — one API call, one config-update.
179
+ ToggleSettingsInit(
180
+ Option<SettingsUpdate>,
181
+ bool,
182
+ Option<Sender<ApiResult<JsValue>>>,
183
+ ),
154
184
  UpdateSession(Box<SessionProps>),
155
185
  UpdateRenderer(Box<RendererProps>),
156
186
  UpdatePresentation(Box<PresentationProps>),
@@ -161,7 +191,16 @@ pub enum PerspectiveViewerMsg {
161
191
  UpdateIsWorkspace(bool),
162
192
 
163
193
  /// Update only `open_column_settings` in the presentation snapshot.
194
+ /// Handled in `settings.rs` (not `snapshots.rs`): a docked-drawer
195
+ /// mount/unmount defers the snapshot behind a presize sweep.
164
196
  UpdateColumnSettings(Box<crate::presentation::OpenColumnSettings>),
197
+
198
+ /// Every visible panel has rendered at its post-transition box — NOW
199
+ /// apply the newest deferred `open_column_settings` target (the
200
+ /// latest-wins slot, not a copy captured at sweep spawn); the `Sender`
201
+ /// resolves on the render commit so the staged presents reveal in the
202
+ /// same paint (mirrors `ToggleSettingsComplete`).
203
+ UpdateColumnSettingsCommit(Sender<()>),
165
204
  UpdateDragDrop(Box<DragDropProps>),
166
205
 
167
206
  /// Update only stats-related fields of `session_props` without touching
@@ -27,7 +27,7 @@ use super::wiring::{
27
27
  clear_active_callbacks, create_active_subscriptions, inject_active_callbacks,
28
28
  subscribe_panel_titles,
29
29
  };
30
- use crate::config::{ViewerConfigInitial, ViewerConfigUpdate};
30
+ use crate::config::ViewerConfigInitial;
31
31
  use crate::queries::*;
32
32
  use crate::renderer::Renderer;
33
33
  use crate::session::*;
@@ -124,7 +124,7 @@ impl PerspectiveViewer {
124
124
  }
125
125
  }
126
126
 
127
- /// Whole-element `restore`'s single commit: the `Workspace` already holds
127
+ /// `restoreWorkspace`'s single commit: the `Workspace` already holds
128
128
  /// the final panel set (models inserted, olds ejected) and the staged
129
129
  /// layout. Re-subscribe the per-panel title wiring (the set changed
130
130
  /// wholesale), activate the restored panel, and re-render — `MainPanel`'s
@@ -193,7 +193,8 @@ impl PerspectiveViewer {
193
193
  })
194
194
  .await?;
195
195
 
196
- let update = ViewerConfigUpdate::decode(&config.encode()?)?;
196
+ // TODO(texodus): what the ****?
197
+ let update = ViewerConfigInitial::decode(&config.encode()?)?;
197
198
  let client = panel.session.get_client();
198
199
  let new_id = create_panel(
199
200
  &elem,
@@ -235,7 +236,7 @@ impl PerspectiveViewer {
235
236
  &workspace,
236
237
  &notify,
237
238
  None,
238
- ViewerConfigInitial::new(table_name).into(),
239
+ ViewerConfigInitial::new(table_name),
239
240
  client,
240
241
  )
241
242
  .await?;
@@ -279,7 +280,7 @@ impl PerspectiveViewer {
279
280
  &workspace,
280
281
  &notify,
281
282
  None,
282
- ViewerConfigInitial::new(table).into(),
283
+ ViewerConfigInitial::new(table),
283
284
  Some(client),
284
285
  )
285
286
  .await?;
@@ -308,11 +309,12 @@ impl PerspectiveViewer {
308
309
  let presentation = ctx.props().presentation.clone();
309
310
  let workspace = ctx.props().workspace.clone();
310
311
  ApiFuture::spawn(async move {
311
- let default_theme = presentation.get_default_theme_name().await;
312
- if let Some(renderer) = workspace.active_renderer()
313
- && let Some(theme) = renderer.theme().or(default_theme)
314
- {
312
+ // Only an EXPLICITLY themed panel mirrors onto the host; an
313
+ // unpinned one inherits whatever the host already shows, so
314
+ // activating it must not restate (and thereby pin) that value.
315
+ if let Some(theme) = workspace.active_renderer().and_then(|x| x.theme()) {
315
316
  presentation.set_theme_name(Some(&theme)).await?;
317
+ presentation.publish_theme_config().await?;
316
318
  }
317
319
 
318
320
  Ok(())
@@ -53,7 +53,7 @@ impl PerspectiveViewer {
53
53
  }
54
54
 
55
55
  let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
56
- locator: c,
56
+ target: c,
57
57
  sender: None,
58
58
  toggle: true,
59
59
  });
@@ -62,7 +62,9 @@ impl PerspectiveViewer {
62
62
  .link()
63
63
  .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
64
64
 
65
- let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
65
+ let on_close_settings = ctx
66
+ .link()
67
+ .callback(|()| ToggleSettingsInit(None, true, None));
66
68
  let on_debug = ctx.link().callback(|_| ToggleDebug);
67
69
  let selected_column = get_current_column_locator(
68
70
  &self.presentation_props.open_column_settings,
@@ -116,7 +118,9 @@ impl PerspectiveViewer {
116
118
  html! { <></> }
117
119
  };
118
120
 
119
- let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
121
+ let on_settings = ctx
122
+ .link()
123
+ .callback(|()| ToggleSettingsInit(None, true, None));
120
124
  let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
121
125
  // Pinning the drawer is a pure CSS positioning flip on `#modal_panel`
122
126
  // (absolute overlay over the main panel <-> static flex sibling
@@ -153,6 +157,7 @@ impl PerspectiveViewer {
153
157
  {presentation}
154
158
  {renderer}
155
159
  {session}
160
+ workspace={ctx.props().workspace.clone()}
156
161
  />
157
162
  <></>
158
163
  </SplitPanel>
@@ -25,7 +25,8 @@ use super::msg::PerspectiveViewerMsg::*;
25
25
  use crate::components::font_loader::FontLoaderStatus;
26
26
  use crate::components::settings_panel::SelectedTab;
27
27
  use crate::config::*;
28
- use crate::presentation::{ColumnLocator, ColumnSettingsTab};
28
+ use crate::presentation::{ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings};
29
+ use crate::queries::get_current_column_locator;
29
30
  use crate::tasks::*;
30
31
 
31
32
  /// The settings sidebar's geometry state, folded into one field on
@@ -69,6 +70,19 @@ pub(super) struct SettingsGeometry {
69
70
  /// than FLOATING over the main panel (the default absolute overlay).
70
71
  /// Session UI state, not part of the saved config.
71
72
  pub column_settings_pinned: bool,
73
+
74
+ /// Latest-wins deferred `open_column_settings` snapshot: the newest
75
+ /// target not yet applied while a docked-drawer presize sweep is in
76
+ /// flight. `UpdateColumnSettingsCommit` applies THIS slot (not a copy
77
+ /// captured at spawn), so a newer target arriving mid-sweep wins and
78
+ /// out-of-order commits are inexpressible.
79
+ pub column_settings_target: Option<OpenColumnSettings>,
80
+ pub column_settings_commit_pending: bool,
81
+
82
+ /// The docked drawer's span, cached whenever it is measurable (pin
83
+ /// toggle, docked unmount) - used to presize for a drawer that MOUNTS
84
+ /// directly into pinned mode, when it isn't in the DOM to measure.
85
+ pub column_settings_docked_width: Option<f64>,
72
86
  }
73
87
 
74
88
  impl PerspectiveViewer {
@@ -76,6 +90,7 @@ impl PerspectiveViewer {
76
90
  &mut self,
77
91
  ctx: &Context<Self>,
78
92
  update: Option<SettingsUpdate>,
93
+ announce: bool,
79
94
  resolve: Option<Sender<ApiResult<JsValue>>>,
80
95
  ) -> bool {
81
96
  match (update, resolve) {
@@ -85,15 +100,15 @@ impl PerspectiveViewer {
85
100
  false
86
101
  },
87
102
  (Some(SettingsUpdate::SetDefault), resolve) => {
88
- self.init_toggle_settings_task(ctx, Some(false), resolve);
103
+ self.init_toggle_settings_task(ctx, Some(false), announce, resolve);
89
104
  false
90
105
  },
91
106
  (Some(SettingsUpdate::Update(force)), resolve) => {
92
- self.init_toggle_settings_task(ctx, Some(force), resolve);
107
+ self.init_toggle_settings_task(ctx, Some(force), announce, resolve);
93
108
  false
94
109
  },
95
110
  (None, resolve) => {
96
- self.init_toggle_settings_task(ctx, None, resolve);
111
+ self.init_toggle_settings_task(ctx, None, announce, resolve);
97
112
  false
98
113
  },
99
114
  }
@@ -150,10 +165,10 @@ impl PerspectiveViewer {
150
165
  &mut self,
151
166
  ctx: &Context<Self>,
152
167
  force: Option<bool>,
168
+ announce: bool,
153
169
  sender: Option<Sender<ApiResult<JsValue>>>,
154
170
  ) {
155
171
  let is_open = ctx.props().presentation.is_settings_open();
156
- ctx.props().presentation.set_settings_before_open(!is_open);
157
172
  match force {
158
173
  Some(force) if is_open == force => {
159
174
  if let Some(sender) = sender {
@@ -161,6 +176,7 @@ impl PerspectiveViewer {
161
176
  }
162
177
  },
163
178
  Some(_) | None => {
179
+ ctx.props().presentation.set_settings_before_open(!is_open);
164
180
  let force = !is_open;
165
181
  let callback = ctx.link().callback(move |resolve| {
166
182
  let update = SettingsUpdate::Update(force);
@@ -195,7 +211,7 @@ impl PerspectiveViewer {
195
211
  let presents = presize_visible_panels_grown(&workspace, &elem).await;
196
212
  let (notify, rendered) = channel::<()>();
197
213
  callback.emit(notify);
198
- presentation.set_settings_open(false);
214
+ presentation.set_settings_open(false, announce);
199
215
  rendered.await?;
200
216
  // `notify` fires in `rendered()` (same task as the
201
217
  // Yew DOM patch) and this future resumes as one of
@@ -219,7 +235,7 @@ impl PerspectiveViewer {
219
235
 
220
236
  let (notify, rendered) = channel::<()>();
221
237
  callback.emit(notify);
222
- presentation.set_settings_open(true);
238
+ presentation.set_settings_open(true, announce);
223
239
  rendered.await?;
224
240
  presents.reveal();
225
241
  resize_visible_panels(&workspace).await;
@@ -339,36 +355,34 @@ impl PerspectiveViewer {
339
355
  pub(super) fn on_open_column_settings(
340
356
  &mut self,
341
357
  ctx: &Context<Self>,
342
- locator: Option<ColumnLocator>,
358
+ target: Option<ColumnSettingsTarget>,
343
359
  sender: Option<Sender<()>>,
344
360
  toggle: bool,
345
361
  ) -> bool {
346
362
  let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
347
- if locator == open_column_settings.locator {
363
+ if target == open_column_settings.target {
348
364
  if toggle {
349
365
  ctx.props().presentation.set_open_column_settings(None);
350
366
  }
351
367
  } else {
352
- open_column_settings.locator.clone_from(&locator);
353
- open_column_settings.tab = if matches!(locator, Some(ColumnLocator::NewExpression)) {
354
- Some(ColumnSettingsTab::Attributes)
355
- } else {
356
- locator.as_ref().and_then(|x| {
357
- x.name().map(|x| {
358
- if self.session_props.is_column_active(x) {
359
- ColumnSettingsTab::Style
360
- } else {
361
- ColumnSettingsTab::Attributes
362
- }
368
+ open_column_settings.target.clone_from(&target);
369
+ open_column_settings.tab = match &target {
370
+ Some(ColumnSettingsTarget::NewExpression) => Some(ColumnSettingsTab::Attributes),
371
+ Some(ColumnSettingsTarget::Column(name)) => {
372
+ Some(if self.session_props.is_column_active(name) {
373
+ ColumnSettingsTab::Style
374
+ } else {
375
+ ColumnSettingsTab::Attributes
363
376
  })
364
- })
377
+ },
378
+ None => None,
365
379
  };
366
380
 
367
381
  ctx.props()
368
382
  .presentation
369
383
  .set_open_column_settings(Some(open_column_settings));
370
384
 
371
- if locator.is_some() {
385
+ if target.is_some() {
372
386
  self.settings_geometry.selected_tab = SelectedTab::Query;
373
387
  }
374
388
  }
@@ -402,9 +416,135 @@ impl PerspectiveViewer {
402
416
  }
403
417
  }
404
418
 
405
- pub(super) fn on_toggle_column_settings_pin(&mut self) -> bool {
419
+ pub(super) fn on_toggle_column_settings_pin(&mut self, ctx: &Context<Self>) -> bool {
420
+ let is_pinned = self.settings_geometry.column_settings_pinned;
421
+ let delta_w = measure_column_settings_pin_delta(&ctx.props().elem, is_pinned);
422
+ if let Some(delta_w) = delta_w {
423
+ self.settings_geometry.column_settings_docked_width = Some(delta_w.abs());
424
+ }
425
+
426
+ self.presize_column_settings_shift(ctx, delta_w, ToggleColumnSettingsPinComplete);
427
+ false
428
+ }
429
+
430
+ pub(super) fn on_toggle_column_settings_pin_complete(&mut self, resolve: Sender<()>) -> bool {
406
431
  self.settings_geometry.column_settings_pinned =
407
432
  !self.settings_geometry.column_settings_pinned;
433
+ self.on_rendered.push(resolve);
434
+ true
435
+ }
436
+
437
+ /// Shared presize choreography for a column-settings geometry shift:
438
+ /// pre-size every visible panel by `delta_w` (`None` = no sweep, commit
439
+ /// only), send `commit_msg` to apply the deferred state on the render
440
+ /// commit, reveal the staged frames in that same paint, then reactively
441
+ /// finalize at the exact settled cells (I6).
442
+ fn presize_column_settings_shift(
443
+ &self,
444
+ ctx: &Context<Self>,
445
+ delta_w: Option<f64>,
446
+ commit_msg: fn(Sender<()>) -> super::msg::PerspectiveViewerMsg,
447
+ ) {
448
+ let workspace = ctx.props().workspace.clone();
449
+ let elem = ctx.props().elem.clone();
450
+ let callback = ctx.link().callback(commit_msg);
451
+ ApiFuture::spawn(async move {
452
+ let presents = match delta_w {
453
+ Some(delta_w) => presize_visible_panels_open(&workspace, &elem, delta_w, 0.0).await,
454
+ None => StagedPresents::default(),
455
+ };
456
+
457
+ let (notify, rendered) = channel::<()>();
458
+ callback.emit(notify);
459
+ rendered.await?;
460
+ presents.reveal();
461
+ resize_visible_panels(&workspace).await;
462
+ Ok(())
463
+ });
464
+ }
465
+
466
+ /// Whether the drawer renders for this `open_column_settings` snapshot
467
+ /// (the `render()` mount predicate). Sensitive to the session snapshot
468
+ /// too — `snapshots.rs` re-evaluates it across an `UpdateSession` apply,
469
+ /// which can invalidate the open column's locator (e.g. a drag
470
+ /// replacing it in `columns`) and unmount the drawer without any
471
+ /// `UpdateColumnSettings` traffic.
472
+ pub(super) fn is_column_settings_mounted(&self, ocs: &OpenColumnSettings) -> bool {
473
+ get_current_column_locator(
474
+ ocs,
475
+ &self.active_renderer,
476
+ &self.session_props.config,
477
+ &self.session_props.metadata,
478
+ )
479
+ .is_some()
480
+ }
481
+
482
+ /// The `open_column_settings` snapshot handler: mount/unmount of the
483
+ /// DOCKED drawer moves `#main_panel_container`'s flex box just like the
484
+ /// pin toggle — and no `before-resize` fires for it — so those
485
+ /// transitions defer the snapshot behind the presize choreography.
486
+ /// Floating transitions and in-place locator changes apply synchronously
487
+ /// as before, unless a deferred commit is in flight (they queue into the
488
+ /// latest-wins slot to preserve commit order).
489
+ pub(super) fn on_update_column_settings(
490
+ &mut self,
491
+ ctx: &Context<Self>,
492
+ ocs: OpenColumnSettings,
493
+ ) -> bool {
494
+ self.refresh_session_snapshot(ctx);
495
+ let effective = self
496
+ .settings_geometry
497
+ .column_settings_target
498
+ .as_ref()
499
+ .unwrap_or(&self.presentation_props.open_column_settings);
500
+ if ocs == *effective {
501
+ return false;
502
+ }
503
+
504
+ let was_mounted =
505
+ self.is_column_settings_mounted(&self.presentation_props.open_column_settings);
506
+ let will_mount = self.is_column_settings_mounted(&ocs);
507
+ let needs_presize =
508
+ self.settings_geometry.column_settings_pinned && was_mounted != will_mount;
509
+ if !needs_presize && !self.settings_geometry.column_settings_commit_pending {
510
+ self.presentation_props.open_column_settings = ocs;
511
+ return true;
512
+ }
513
+
514
+ self.settings_geometry.column_settings_target = Some(ocs);
515
+ if self.settings_geometry.column_settings_commit_pending {
516
+ return false;
517
+ }
518
+
519
+ self.settings_geometry.column_settings_commit_pending = true;
520
+ let delta_w = if !needs_presize {
521
+ None
522
+ } else if was_mounted {
523
+ // Unmounting - the docked drawer is still in the DOM, and its
524
+ // freed span is the same measurement as an unpin.
525
+ let delta = measure_column_settings_pin_delta(&ctx.props().elem, true);
526
+ if let Some(delta) = delta {
527
+ self.settings_geometry.column_settings_docked_width = Some(delta.abs());
528
+ }
529
+
530
+ delta
531
+ } else {
532
+ // Mounting directly into pinned mode - not yet in the DOM; the
533
+ // span cached at the last dock/unmount predicts the shrink.
534
+ self.settings_geometry.column_settings_docked_width
535
+ };
536
+
537
+ self.presize_column_settings_shift(ctx, delta_w, UpdateColumnSettingsCommit);
538
+ false
539
+ }
540
+
541
+ pub(super) fn on_update_column_settings_commit(&mut self, resolve: Sender<()>) -> bool {
542
+ self.settings_geometry.column_settings_commit_pending = false;
543
+ self.on_rendered.push(resolve);
544
+ if let Some(ocs) = self.settings_geometry.column_settings_target.take() {
545
+ self.presentation_props.open_column_settings = ocs;
546
+ }
547
+
408
548
  true
409
549
  }
410
550
 
@@ -15,17 +15,45 @@
15
15
  //! subscriptions/callbacks) into the root's props fields, re-rendering only
16
16
  //! on an actual change.
17
17
 
18
+ use futures::channel::oneshot::channel;
19
+ use perspective_js::utils::ApiFuture;
18
20
  use yew::prelude::*;
19
21
 
20
22
  use super::PerspectiveViewer;
21
- use crate::presentation::{DragDropProps, OpenColumnSettings, PresentationProps};
23
+ use crate::presentation::{DragDropProps, PresentationProps};
22
24
  use crate::renderer::RendererProps;
23
25
  use crate::session::{SessionProps, TableLoadState, ViewStats};
26
+ use crate::tasks::resize_visible_panels;
24
27
 
25
28
  impl PerspectiveViewer {
26
- pub(super) fn on_update_session(&mut self, props: SessionProps) -> bool {
29
+ pub(super) fn refresh_session_snapshot(&mut self, ctx: &Context<Self>) {
30
+ let props = self.active_session.to_props();
31
+ if props != self.session_props {
32
+ self.on_update_session(ctx, props);
33
+ }
34
+ }
35
+
36
+ pub(super) fn on_update_session(&mut self, ctx: &Context<Self>, props: SessionProps) -> bool {
27
37
  let changed = props != self.session_props;
38
+ let was_mounted =
39
+ self.is_column_settings_mounted(&self.presentation_props.open_column_settings);
28
40
  self.session_props = props;
41
+ let now_mounted =
42
+ self.is_column_settings_mounted(&self.presentation_props.open_column_settings);
43
+ if self.settings_geometry.column_settings_pinned
44
+ && was_mounted != now_mounted
45
+ && !self.settings_geometry.column_settings_commit_pending
46
+ {
47
+ let (notify, rendered) = channel::<()>();
48
+ self.on_rendered.push(notify);
49
+ let workspace = ctx.props().workspace.clone();
50
+ ApiFuture::spawn(async move {
51
+ rendered.await?;
52
+ resize_visible_panels(&workspace).await;
53
+ Ok(())
54
+ });
55
+ }
56
+
29
57
  changed
30
58
  }
31
59
 
@@ -52,18 +80,17 @@ impl PerspectiveViewer {
52
80
  ctx: &Context<Self>,
53
81
  props: PresentationProps,
54
82
  ) -> bool {
55
- // Default-theme fan-out: when the registry default (first available
56
- // theme) changes — the async theme discovery resolving at boot, or a
57
- // `resetThemes` — push the new default into every panel's renderer
58
- // cache (locked draws stamp the effective theme from it), and
59
- // restyle the panels whose captured `--psp-*` CSS is STALE against
60
- // the new effective value (`Renderer::needs_restyle` — the plugin's
61
- // captured theme is the baseline; plugins only re-read CSS at
62
- // `restyle()`/first-draw, so a plain redraw would not repaint a
63
- // panel that first drew before discovery resolved, while a panel
64
- // that captured post-discovery — or owes its first paint — restyles
65
- // nothing). The outer default-diff scopes the scan; the per-panel
66
- // gate is state, never call history or DOM state.
83
+ // Boot fill-in: theme discovery resolving turns an empty registry
84
+ // into a real one, and any panel created before that has no theme to
85
+ // stamp. Give those — and ONLY those — the new default, then restyle
86
+ // the ones whose captured `--psp-*` CSS is now stale
87
+ // (`Renderer::needs_restyle`; plugins re-read CSS only at
88
+ // `restyle()`/first-draw, so a plain redraw would not repaint them).
89
+ //
90
+ // A panel that already has a theme is never touched here: `theme` is
91
+ // concrete state, so re-ordering the registry — or any other
92
+ // `resetThemes` that leaves a panel's theme available — must repaint
93
+ // nothing.
67
94
  let old_default = self.presentation_props.available_themes.first().cloned();
68
95
  let new_default = props.available_themes.first().cloned();
69
96
  if old_default != new_default {
@@ -74,7 +101,11 @@ impl PerspectiveViewer {
74
101
  .into_iter()
75
102
  .filter_map(|id| ctx.props().workspace.panel(&id))
76
103
  {
77
- panel.renderer.set_default_theme(new_default.clone());
104
+ if panel.renderer.theme().is_some() {
105
+ continue;
106
+ }
107
+
108
+ panel.renderer.set_theme(new_default.clone());
78
109
  if panel.renderer.needs_restyle() {
79
110
  let renderer = panel.renderer.clone();
80
111
  crate::utils::spawn_owned("default-theme-restyle", async move {
@@ -102,12 +133,6 @@ impl PerspectiveViewer {
102
133
  changed
103
134
  }
104
135
 
105
- pub(super) fn on_update_column_settings(&mut self, ocs: OpenColumnSettings) -> bool {
106
- let changed = ocs != self.presentation_props.open_column_settings;
107
- self.presentation_props.open_column_settings = ocs;
108
- changed
109
- }
110
-
111
136
  pub(super) fn on_update_dragdrop(&mut self, props: DragDropProps) -> bool {
112
137
  let changed = props != self.dragdrop_props;
113
138
  self.dragdrop_props = props;
@@ -343,7 +343,9 @@ pub(super) fn create_active_subscriptions(
343
343
  pub(super) fn create_shared_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
344
344
  let presentation_props_sub = {
345
345
  let presentation = ctx.props().presentation.clone();
346
- let cb_settings = ctx.link().callback(UpdateSettingsOpen);
346
+ let cb_settings = ctx
347
+ .link()
348
+ .callback(|(open, _): (bool, bool)| UpdateSettingsOpen(open));
347
349
  let cb_theme = {
348
350
  let pres = presentation.clone();
349
351
  ctx.link()
@@ -368,17 +370,6 @@ pub(super) fn create_shared_subscriptions(ctx: &Context<PerspectiveViewer>) -> V
368
370
  vec![sub1, sub2, sub3]
369
371
  };
370
372
 
371
- let dragdrop_props_sub = {
372
- let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
373
- let sub1 = ctx
374
- .props()
375
- .presentation
376
- .drop_received
377
- .add_notify_listener(&cb_clear);
378
-
379
- vec![sub1]
380
- };
381
-
382
373
  // The `Workspace`-owned global filter set → the root's render snapshot.
383
374
  let filters_sub = {
384
375
  let cb = ctx.link().callback(|_: ()| UpdateGlobalFilters);
@@ -388,11 +379,12 @@ pub(super) fn create_shared_subscriptions(ctx: &Context<PerspectiveViewer>) -> V
388
379
  .add_notify_listener(&cb)
389
380
  };
390
381
 
391
- // Staged-set transitions (`Workspace::stage_panel`/`clear_staged`) → a
392
- // root re-render for the stage-level chrome (`only-child` class,
393
- // binding resync). `MainPanel` inserts/reveals promoted panels via its
394
- // OWN `staged_changed` subscription (`MainPanelMsg::StagedChanged`) —
395
- // see `WorkspaceData::staged_changed`.
382
+ // Panel-phase transitions (`Workspace::insert_panel` at
383
+ // `PanelPhase::Staging` / `Workspace::promote`) → a root re-render for
384
+ // the stage-level chrome (`only-child` class, binding resync).
385
+ // `MainPanel` inserts/reveals promoted panels via its OWN
386
+ // `staged_changed` subscription (`MainPanelMsg::StagedChanged`) — see
387
+ // `WorkspaceData::staged_changed`.
396
388
  let staged_sub = {
397
389
  let cb = ctx.link().callback(|_: ()| LayoutChanged);
398
390
  ctx.props()
@@ -403,7 +395,6 @@ pub(super) fn create_shared_subscriptions(ctx: &Context<PerspectiveViewer>) -> V
403
395
 
404
396
  let mut subscriptions = Vec::new();
405
397
  subscriptions.extend(presentation_props_sub);
406
- subscriptions.extend(dragdrop_props_sub);
407
398
  subscriptions.push(filters_sub);
408
399
  subscriptions.push(staged_sub);
409
400
  subscriptions
@@ -446,15 +437,13 @@ pub(super) fn inject_active_callbacks(
446
437
  let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
447
438
  let mut msgs = vec![UpdateRenderer(Box::new(r.to_props(Some(limits))))];
448
439
  if !limits.is_update {
449
- let locator = get_current_column_locator(
450
- &presentation.get_open_column_settings(),
451
- &r,
452
- &s.get_view_config(),
453
- &s.metadata(),
454
- );
440
+ let ocs = presentation.get_open_column_settings();
441
+ let still_open =
442
+ get_current_column_locator(&ocs, &r, &s.get_view_config(), &s.metadata())
443
+ .is_some();
455
444
 
456
445
  msgs.push(OpenColumnSettings {
457
- locator,
446
+ target: ocs.target.filter(|_| still_open),
458
447
  sender: None,
459
448
  toggle: false,
460
449
  });