@perspective-dev/viewer 4.5.2 → 5.1.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 (227) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  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/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/extensions.d.ts +22 -3
  24. package/dist/esm/perspective-viewer.d.ts +9 -0
  25. package/dist/esm/perspective-viewer.inline.js +20 -1
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +20 -1
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/plugin.d.ts +71 -5
  30. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  32. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  33. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  34. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  35. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  36. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  37. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  38. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -0
  39. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  40. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  41. package/dist/esm/ts-rs/ViewerConfig.d.ts +8 -2
  42. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
  43. package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
  44. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  45. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  46. package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
  47. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  50. package/dist/wasm/perspective-viewer.d.ts +299 -78
  51. package/dist/wasm/perspective-viewer.js +544 -186
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  54. package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
  55. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
  56. package/package.json +3 -2
  57. package/src/css/_context_menu.css +14 -0
  58. package/src/css/column-selector.css +1 -0
  59. package/src/css/column-settings-panel.css +68 -3
  60. package/src/css/column-style.css +4 -3
  61. package/src/css/config-selector.css +22 -1
  62. package/src/css/containers/context-menu.css +107 -0
  63. package/src/css/containers/tabs.css +2 -2
  64. package/src/css/panel-tab.css +193 -0
  65. package/src/css/plugin-selector.css +7 -10
  66. package/src/css/status-bar.css +75 -81
  67. package/src/css/viewer.css +188 -70
  68. package/src/rust/components/column_dropdown.rs +40 -30
  69. package/src/rust/components/column_selector/active_column.rs +28 -45
  70. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  71. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  72. package/src/rust/components/column_selector/config_selector.rs +40 -82
  73. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  74. package/src/rust/components/column_selector/filter_column.rs +5 -15
  75. package/src/rust/components/column_selector/inactive_column.rs +11 -9
  76. package/src/rust/components/column_selector/sort_column.rs +3 -6
  77. package/src/rust/components/column_selector.rs +25 -14
  78. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  79. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  80. package/src/rust/components/column_settings_sidebar.rs +163 -16
  81. package/src/rust/components/containers/dragdrop_list.rs +73 -3
  82. package/src/rust/components/containers/sidebar.rs +99 -22
  83. package/src/rust/components/containers/split_panel.rs +181 -62
  84. package/src/rust/components/context_menu.rs +130 -0
  85. package/src/rust/components/editable_header.rs +2 -0
  86. package/src/rust/components/form/debug.rs +3 -0
  87. package/src/rust/components/global_filter_bar.rs +76 -0
  88. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  89. package/src/rust/components/main_panel/msg.rs +62 -0
  90. package/src/rust/components/main_panel/presize.rs +220 -0
  91. package/src/rust/components/main_panel/reconcile.rs +224 -0
  92. package/src/rust/components/main_panel/render.rs +385 -0
  93. package/src/rust/components/main_panel/update.rs +184 -0
  94. package/src/rust/components/main_panel.rs +316 -71
  95. package/src/rust/components/mod.rs +5 -0
  96. package/src/rust/components/panel_menu.rs +493 -0
  97. package/src/rust/components/panel_tab.rs +510 -0
  98. package/src/rust/components/plugin_tab.rs +1 -1
  99. package/src/rust/components/portal.rs +7 -0
  100. package/src/rust/components/settings_panel.rs +13 -5
  101. package/src/rust/components/status_bar.rs +110 -178
  102. package/src/rust/components/status_indicator.rs +6 -9
  103. package/src/rust/components/style/mod.rs +5 -0
  104. package/src/rust/components/style/style_provider.rs +2 -0
  105. package/src/rust/components/viewer/filters.rs +133 -0
  106. package/src/rust/components/viewer/msg.rs +182 -0
  107. package/src/rust/components/viewer/panels.rs +326 -0
  108. package/src/rust/components/viewer/render.rs +255 -0
  109. package/src/rust/components/viewer/settings.rs +434 -0
  110. package/src/rust/components/viewer/snapshots.rs +134 -0
  111. package/src/rust/components/viewer/wiring.rs +505 -0
  112. package/src/rust/components/viewer.rs +245 -756
  113. package/src/rust/components/window_editor.rs +1013 -0
  114. package/src/rust/config/export_method.rs +1 -1
  115. package/src/rust/config/mod.rs +4 -0
  116. package/src/rust/config/options.rs +71 -0
  117. package/src/rust/config/viewer_config.rs +24 -2
  118. package/src/rust/config/workspace_config.rs +92 -0
  119. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  120. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  121. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  122. package/src/rust/custom_elements/viewer.rs +1015 -286
  123. package/src/rust/custom_events.rs +212 -61
  124. package/src/rust/js/mod.rs +2 -0
  125. package/src/rust/js/plugin.rs +297 -10
  126. package/src/rust/js/regular_layout.rs +258 -0
  127. package/src/rust/lib.rs +24 -2
  128. package/src/rust/presentation/column_locator.rs +16 -1
  129. package/src/rust/presentation.rs +66 -33
  130. package/src/rust/queries/columns_iter_set.rs +7 -1
  131. package/src/rust/queries/get_viewer_config.rs +29 -11
  132. package/src/rust/queries/plugin_column_styles.rs +2 -2
  133. package/src/rust/renderer/activate.rs +41 -14
  134. package/src/rust/renderer/dispatch.rs +463 -0
  135. package/src/rust/renderer/plugin_config.rs +495 -0
  136. package/src/rust/renderer/registry.rs +18 -6
  137. package/src/rust/renderer/render_timer.rs +29 -10
  138. package/src/rust/renderer.rs +384 -838
  139. package/src/rust/session/column_defaults_update.rs +26 -8
  140. package/src/rust/session/drag_drop_update.rs +18 -0
  141. package/src/rust/session/metadata.rs +66 -9
  142. package/src/rust/session/replace_expression_update.rs +1 -0
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +755 -264
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/eject.rs +64 -23
  153. package/src/rust/tasks/mod.rs +30 -4
  154. package/src/rust/tasks/pipeline.rs +411 -0
  155. package/src/rust/tasks/presize_panels.rs +324 -0
  156. package/src/rust/tasks/reset_all.rs +22 -12
  157. package/src/rust/tasks/resize_observer.rs +124 -87
  158. package/src/rust/tasks/restore_and_render.rs +80 -86
  159. package/src/rust/tasks/restore_panel.rs +145 -0
  160. package/src/rust/tasks/send_column_config.rs +5 -2
  161. package/src/rust/tasks/send_plugin_config.rs +5 -2
  162. package/src/rust/tasks/set_edit_mode.rs +59 -0
  163. package/src/rust/tasks/sync_update_panels.rs +152 -0
  164. package/src/rust/tasks/table_lifecycle.rs +158 -0
  165. package/src/rust/tasks/update_theme.rs +56 -12
  166. package/src/rust/utils/browser/dragdrop.rs +12 -0
  167. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  168. package/src/rust/utils/completion.rs +56 -0
  169. package/src/rust/utils/custom_element.rs +5 -0
  170. package/src/rust/utils/debounce.rs +189 -17
  171. package/src/rust/utils/mod.rs +6 -0
  172. package/src/rust/utils/pending_effects.rs +62 -0
  173. package/src/rust/utils/spawn.rs +33 -0
  174. package/src/rust/utils/weak_scope.rs +0 -10
  175. package/src/rust/workspace.rs +846 -0
  176. package/src/svg/active-indicator.svg +3 -0
  177. package/src/svg/pin-icon.svg +5 -0
  178. package/src/themes/botanical.css +5 -2
  179. package/src/themes/defaults.css +54 -2
  180. package/src/themes/dracula.css +6 -3
  181. package/src/themes/gruvbox-dark.css +5 -2
  182. package/src/themes/gruvbox.css +5 -2
  183. package/src/themes/icons.css +5 -2
  184. package/src/themes/intl/de.css +7 -0
  185. package/src/themes/intl/es.css +7 -0
  186. package/src/themes/intl/fr.css +7 -0
  187. package/src/themes/intl/ja.css +7 -0
  188. package/src/themes/intl/pt.css +7 -0
  189. package/src/themes/intl/zh.css +7 -0
  190. package/src/themes/intl.css +10 -1
  191. package/src/themes/monokai.css +5 -2
  192. package/src/themes/phosphor.css +5 -2
  193. package/src/themes/pro-dark.css +5 -2
  194. package/src/themes/pro.css +2 -1
  195. package/src/themes/solarized-dark.css +5 -2
  196. package/src/themes/solarized.css +5 -2
  197. package/src/themes/vaporwave.css +5 -2
  198. package/src/ts/bootstrap.ts +8 -7
  199. package/src/ts/extensions.ts +30 -2
  200. package/src/ts/perspective-viewer.ts +10 -0
  201. package/src/ts/plugin.ts +72 -5
  202. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  203. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  204. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  205. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  206. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  207. package/src/ts/ts-rs/Layout.ts +9 -0
  208. package/src/ts/ts-rs/Orientation.ts +6 -0
  209. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  210. package/src/ts/ts-rs/PanelViewerConfig.ts +16 -0
  211. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  212. package/src/ts/ts-rs/TabLayout.ts +11 -0
  213. package/src/ts/ts-rs/ViewerConfig.ts +7 -2
  214. package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
  215. package/src/ts/ts-rs/WindowAggregate.ts +3 -0
  216. package/src/ts/ts-rs/WindowSort.ts +9 -0
  217. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  218. package/src/ts/ts-rs/WindowSpec.ts +46 -0
  219. package/src/ts/ts-rs/Windows.ts +10 -0
  220. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  221. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  222. package/src/rust/tasks/intersection_observer.rs +0 -98
  223. package/src/rust/tasks/update_and_render.rs +0 -110
  224. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
  225. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
  226. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
  227. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
@@ -10,31 +10,43 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ //! The root `<perspective-viewer>` Yew component: state, lifecycle, and the
14
+ //! message dispatch table. Handler bodies live in the domain modules —
15
+ //! [`panels`] (workspace panel lifecycle + active targeting), [`settings`]
16
+ //! (settings sidebar + divider presize pump), [`filters`] (master/detail
17
+ //! cross-filter), [`snapshots`] (value-semantic props plumbing) — with engine
18
+ //! wiring in [`wiring`] and `view()` in [`render`]. (The panel context menu +
19
+ //! pickers + maximize live in `MainPanel`/`PanelMenu`, which own the layout
20
+ //! element.)
21
+
22
+ mod filters;
23
+ mod msg;
24
+ mod panels;
25
+ mod render;
26
+ mod settings;
27
+ mod snapshots;
28
+ mod wiring;
29
+
13
30
  use std::rc::Rc;
14
31
 
15
- use futures::channel::oneshot::*;
32
+ use futures::channel::oneshot::Sender;
33
+ use perspective_client::config::Filter;
16
34
  use perspective_js::utils::*;
17
35
  use wasm_bindgen::JsCast;
18
36
  use wasm_bindgen::prelude::*;
19
- use web_sys::{FocusEvent, KeyboardEvent};
20
37
  use yew::prelude::*;
21
38
 
22
- use super::containers::split_panel::SplitPanel;
23
- use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
24
- use super::style::StyleProvider;
25
- use crate::components::column_settings_sidebar::ColumnSettingsPanel;
26
- use crate::components::main_panel::MainPanel;
27
- use crate::components::settings_panel::{SelectedTab, SettingsPanel};
28
- use crate::config::*;
29
- use crate::js::JsPerspectiveViewerPlugin;
30
- use crate::presentation::{
31
- ColumnLocator, ColumnSettingsTab, DragDropProps, Presentation, PresentationProps,
32
- };
33
- use crate::queries::*;
39
+ pub use self::msg::PerspectiveViewerMsg;
40
+ use self::msg::PerspectiveViewerMsg::*;
41
+ use self::settings::SettingsGeometry;
42
+ use self::wiring::*;
43
+ use super::font_loader::{FontLoaderProps, FontLoaderStatus};
44
+ use crate::presentation::{Presentation, PresentationProps};
34
45
  use crate::renderer::{RendererProps, *};
35
46
  use crate::session::{SessionProps, *};
36
47
  use crate::tasks::*;
37
48
  use crate::utils::*;
49
+ use crate::workspace::Workspace;
38
50
 
39
51
  #[derive(Clone, Properties)]
40
52
  pub struct PerspectiveViewerProps {
@@ -42,9 +54,12 @@ pub struct PerspectiveViewerProps {
42
54
  pub elem: web_sys::HtmlElement,
43
55
 
44
56
  /// State
45
- pub session: Session,
46
- pub renderer: Renderer,
47
57
  pub presentation: Presentation,
58
+
59
+ /// The multi-panel model — the source of truth for engine state. The
60
+ /// component derives the active panel's `Session`/`Renderer` via
61
+ /// `workspace.active_*()` (see `create`), and renders a cell per panel.
62
+ pub workspace: Workspace,
48
63
  }
49
64
 
50
65
  impl PartialEq for PerspectiveViewerProps {
@@ -53,63 +68,47 @@ impl PartialEq for PerspectiveViewerProps {
53
68
  }
54
69
  }
55
70
 
56
- #[derive(Debug)]
57
- pub enum PerspectiveViewerMsg {
58
- ColumnSettingsPanelSizeUpdate(Option<i32>),
59
- ColumnSettingsTabChanged(ColumnSettingsTab),
60
- OpenColumnSettings {
61
- locator: Option<ColumnLocator>,
62
- sender: Option<Sender<()>>,
63
- toggle: bool,
64
- },
65
- PreloadFontsUpdate,
66
- Reset(bool, Option<Sender<()>>),
67
- Resize,
68
- SettingsPanelSizeUpdate(Option<i32>),
69
- SettingsPanelTabChanged(SelectedTab),
70
- SettingsPanelAutoWidth(f64),
71
- ToggleDebug,
72
- ToggleSettingsComplete(SettingsUpdate, Sender<()>),
73
- ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
74
- UpdateSession(Box<SessionProps>),
75
- UpdateRenderer(Box<RendererProps>),
76
- UpdatePresentation(Box<PresentationProps>),
77
-
78
- /// Update only `is_settings_open` in the presentation snapshot without
79
- /// touching `available_themes` (which requires async data).
80
- UpdateSettingsOpen(bool),
81
- UpdateIsWorkspace(bool),
82
-
83
- /// Update only `open_column_settings` in the presentation snapshot.
84
- UpdateColumnSettings(Box<crate::presentation::OpenColumnSettings>),
85
- UpdateDragDrop(Box<DragDropProps>),
86
-
87
- /// Update only stats-related fields of `session_props` without touching
88
- /// `config`. This prevents `stats_changed` events (e.g. from `reset()`)
89
- /// from propagating a freshly-cleared config to the column selector.
90
- UpdateSessionStats(Option<ViewStats>, Option<TableLoadState>),
91
-
92
- /// Increment/decrement the in-flight render counter threaded to
93
- /// `StatusIndicator` so it can show the "updating" spinner.
94
- IncrementUpdateCount,
95
- DecrementUpdateCount,
96
- }
97
-
98
- use PerspectiveViewerMsg::*;
99
-
100
71
  pub struct PerspectiveViewer {
101
72
  _subscriptions: Vec<Subscription>,
102
- column_settings_panel_width_override: Option<i32>,
73
+
74
+ /// Per-active-panel subscriptions; dropped + recreated on `SetActivePanel`.
75
+ _active_subscriptions: Vec<Subscription>,
76
+
77
+ /// One `title_changed` subscription per panel (all panels, not just
78
+ /// active), so a title change on any panel re-renders the tab titles.
79
+ /// Recreated when the panel set changes (`LayoutChanged`/`ClosePanel`).
80
+ _title_subscriptions: Vec<Subscription>,
81
+
82
+ /// The active panel's engine handles — what the settings panel + status bar
83
+ /// bind to. Re-targeted on `SetActivePanel`. Fall back to
84
+ /// `empty_session`/`empty_renderer` when the element has zero panels.
85
+ active_session: Session,
86
+ active_renderer: Renderer,
87
+
88
+ /// Detached "empty" engine handles (never inserted into the `Workspace`,
89
+ /// never mounted). Used as the `active_*` fallback when there are zero
90
+ /// panels, so the settings/status chrome always has a `Session`+`Renderer`
91
+ /// to bind to — and, being tableless, renders an inert empty state.
92
+ empty_session: Session,
93
+ empty_renderer: Renderer,
103
94
  debug_open: bool,
104
95
  fonts: FontLoaderProps,
105
96
  on_close_column_settings: Callback<()>,
106
- on_rendered: Option<Sender<()>>,
97
+ on_rendered: Vec<Sender<()>>,
107
98
  on_resize: Rc<PubSub<()>>,
108
99
  on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
109
100
  settings_open: bool,
110
- settings_panel_width_override: Option<i32>,
111
- settings_panel_selected_tab: SelectedTab,
112
- settings_panel_auto_width: f64,
101
+
102
+ /// Render snapshot of the `Workspace`-owned global filter set (see
103
+ /// `Workspace::global_filters`); refreshed by `UpdateGlobalFilters` via
104
+ /// the `filters_changed` subscription. The status-bar chips render from
105
+ /// this when non-empty.
106
+ global_filters: Vec<Filter>,
107
+
108
+ /// The settings sidebar's geometry state (pane/drawer width overrides,
109
+ /// divider presize pump, open-state deltas cache) — see
110
+ /// [`settings::SettingsGeometry`].
111
+ settings_geometry: SettingsGeometry,
113
112
 
114
113
  /// Value-semantic state snapshots (Step 4 scaffold).
115
114
  /// Populated by `UpdateSession` / `UpdateRenderer` / `UpdatePresentation` /
@@ -117,10 +116,12 @@ pub struct PerspectiveViewer {
117
116
  session_props: SessionProps,
118
117
  renderer_props: RendererProps,
119
118
  presentation_props: PresentationProps,
120
- dragdrop_props: DragDropProps,
119
+ dragdrop_props: crate::presentation::DragDropProps,
121
120
 
122
- /// Counts in-flight renders (incremented on `view_config_changed`,
123
- /// decremented on `view_created`). Threaded to `StatusIndicator`.
121
+ /// The active panel's in-flight config-run count — a LEVEL-triggered
122
+ /// snapshot of `Session::in_flight_config_runs` (RAII-settled;
123
+ /// assigned by `UpdateInFlight`, re-read on retarget). Threaded to
124
+ /// `StatusIndicator` as the "updating" spinner.
124
125
  update_count: u32,
125
126
 
126
127
  /// Window listeners that toggle the `.shift-active` class on the host
@@ -128,64 +129,13 @@ pub struct PerspectiveViewer {
128
129
  /// (e.g. inactive column add, active column remove, status-bar reset)
129
130
  /// visually discoverable. Stored so the closures outlive `create`.
130
131
  _shift_listeners: ShiftListeners,
131
- }
132
-
133
- struct ShiftListeners {
134
- elem: web_sys::HtmlElement,
135
- keydown: Closure<dyn FnMut(KeyboardEvent)>,
136
- keyup: Closure<dyn FnMut(KeyboardEvent)>,
137
- blur: Closure<dyn FnMut(FocusEvent)>,
138
- }
139
132
 
140
- impl Drop for ShiftListeners {
141
- fn drop(&mut self) {
142
- let win = global::window();
143
- let _ = win
144
- .remove_event_listener_with_callback("keydown", self.keydown.as_ref().unchecked_ref());
145
- let _ =
146
- win.remove_event_listener_with_callback("keyup", self.keyup.as_ref().unchecked_ref());
147
- let _ = win.remove_event_listener_with_callback("blur", self.blur.as_ref().unchecked_ref());
148
- let _ = self.elem.class_list().remove_1("shift-active");
149
- }
150
- }
151
-
152
- fn install_shift_listeners(elem: web_sys::HtmlElement) -> ShiftListeners {
153
- let keydown = {
154
- let elem = elem.clone();
155
- Closure::wrap(Box::new(move |event: KeyboardEvent| {
156
- if event.key() == "Shift" {
157
- let _ = elem.class_list().add_1("shift-active");
158
- }
159
- }) as Box<dyn FnMut(KeyboardEvent)>)
160
- };
161
-
162
- let keyup = {
163
- let elem = elem.clone();
164
- Closure::wrap(Box::new(move |event: KeyboardEvent| {
165
- if event.key() == "Shift" {
166
- let _ = elem.class_list().remove_1("shift-active");
167
- }
168
- }) as Box<dyn FnMut(KeyboardEvent)>)
169
- };
170
-
171
- let blur = {
172
- let elem = elem.clone();
173
- Closure::wrap(Box::new(move |_: FocusEvent| {
174
- let _ = elem.class_list().remove_1("shift-active");
175
- }) as Box<dyn FnMut(FocusEvent)>)
176
- };
177
-
178
- let win = global::window();
179
- let _ = win.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref());
180
- let _ = win.add_event_listener_with_callback("keyup", keyup.as_ref().unchecked_ref());
181
- let _ = win.add_event_listener_with_callback("blur", blur.as_ref().unchecked_ref());
182
-
183
- ShiftListeners {
184
- elem,
185
- keydown,
186
- keyup,
187
- blur,
188
- }
133
+ /// `perspective-global-filter` + `perspective-click` listeners on the host
134
+ /// (master/detail). Both route a master panel's selection state to
135
+ /// `MasterContribution`. Kept alive here, attached once in `rendered`.
136
+ _global_filter_listener: Closure<dyn FnMut(web_sys::Event)>,
137
+ _master_click_listener: Closure<dyn FnMut(web_sys::Event)>,
138
+ master_listeners_attached: bool,
189
139
  }
190
140
 
191
141
  impl Component for PerspectiveViewer {
@@ -195,13 +145,26 @@ impl Component for PerspectiveViewer {
195
145
  fn create(ctx: &Context<Self>) -> Self {
196
146
  let elem = ctx.props().elem.clone();
197
147
  let fonts = FontLoaderProps::new(&elem, ctx.link().callback(|()| PreloadFontsUpdate));
198
- inject_engine_callbacks(ctx);
199
- let subscriptions = create_subscriptions(ctx);
200
- let session_props = ctx.props().session.to_props();
201
- let renderer_props = ctx.props().renderer.to_props(None);
148
+ let empty_session = Session::new();
149
+ let empty_renderer = Renderer::new(&elem);
150
+ let active_session = ctx
151
+ .props()
152
+ .workspace
153
+ .active_session()
154
+ .unwrap_or_else(|| empty_session.clone());
155
+ let active_renderer = ctx
156
+ .props()
157
+ .workspace
158
+ .active_renderer()
159
+ .unwrap_or_else(|| empty_renderer.clone());
160
+ inject_shared_callbacks(ctx);
161
+ inject_active_callbacks(ctx, &active_session, &active_renderer);
162
+ let subscriptions = create_shared_subscriptions(ctx);
163
+ let active_subscriptions =
164
+ create_active_subscriptions(ctx, &active_session, &active_renderer);
165
+ let session_props = active_session.to_props();
166
+ let renderer_props = active_renderer.to_props(None);
202
167
  let presentation_props = ctx.props().presentation.to_props(PtrEqRc::new(vec![]));
203
-
204
- // Memoized callback for column settings drawer
205
168
  let on_close_column_settings = ctx.link().callback(|_| OpenColumnSettings {
206
169
  locator: None,
207
170
  sender: None,
@@ -226,231 +189,171 @@ impl Component for PerspectiveViewer {
226
189
  }
227
190
 
228
191
  let shift_listeners = install_shift_listeners(elem);
192
+ let global_filter_listener = wiring::global_filter_listener(ctx);
193
+ let master_click_listener = wiring::master_click_listener(ctx);
229
194
 
230
195
  Self {
231
196
  _subscriptions: subscriptions,
232
- column_settings_panel_width_override: None,
197
+ _global_filter_listener: global_filter_listener,
198
+ _master_click_listener: master_click_listener,
199
+ master_listeners_attached: false,
200
+ _active_subscriptions: active_subscriptions,
201
+ _title_subscriptions: subscribe_panel_titles(ctx),
202
+ active_session,
203
+ active_renderer,
204
+ empty_session,
205
+ empty_renderer,
233
206
  debug_open: false,
234
207
  fonts,
235
208
  on_close_column_settings,
236
- on_rendered: None,
209
+ on_rendered: Vec::new(),
237
210
  on_resize: Default::default(),
238
211
  on_settings_panel_dimensions_reset: Default::default(),
239
212
  settings_open: false,
240
- settings_panel_width_override: None,
241
- settings_panel_selected_tab: SelectedTab::default(),
242
- settings_panel_auto_width: 0.0,
213
+ global_filters: Vec::new(),
214
+ settings_geometry: Default::default(),
243
215
  session_props,
244
216
  renderer_props,
245
217
  presentation_props,
246
- dragdrop_props: DragDropProps::default(),
218
+ dragdrop_props: Default::default(),
247
219
  update_count: 0,
248
220
  _shift_listeners: shift_listeners,
249
221
  }
250
222
  }
251
223
 
224
+ /// The protocol dispatch table: every arm is a one-line call into a domain
225
+ /// module (see the module doc). Trivial self-describing arms stay inline.
252
226
  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
253
227
  match msg {
254
228
  PreloadFontsUpdate => true,
229
+ TitlesChanged => true,
255
230
  Resize => {
256
231
  self.on_resize.emit(());
257
232
  false
258
233
  },
259
- Reset(all, sender) => {
260
- reset_all(
261
- &ctx.props().session,
262
- &ctx.props().renderer,
263
- &ctx.props().presentation,
264
- all,
265
- sender,
266
- );
267
- false
268
- },
269
- ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
270
- ToggleSettingsInit(Some(SettingsUpdate::Missing), Some(resolve)) => {
271
- resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
272
- false
273
- },
274
- ToggleSettingsInit(Some(SettingsUpdate::SetDefault), resolve) => {
275
- self.init_toggle_settings_task(ctx, Some(false), resolve);
276
- false
277
- },
278
- ToggleSettingsInit(Some(SettingsUpdate::Update(force)), resolve) => {
279
- self.init_toggle_settings_task(ctx, Some(force), resolve);
280
- false
281
- },
282
- ToggleSettingsInit(None, resolve) => {
283
- self.init_toggle_settings_task(ctx, None, resolve);
284
- false
285
- },
286
- ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => {
287
- ctx.props().presentation.set_open_column_settings(None);
288
- self.settings_open = false;
289
- self.on_rendered = Some(resolve);
290
- true
291
- },
292
- ToggleSettingsComplete(SettingsUpdate::Update(force), resolve)
293
- if force != self.settings_open =>
294
- {
295
- ctx.props().presentation.set_open_column_settings(None);
296
- self.settings_open = force;
297
- self.on_rendered = Some(resolve);
298
- true
299
- },
300
- ToggleSettingsComplete(_, resolve)
301
- if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) =>
302
- {
303
- if let Err(e) = resolve.send(()) {
304
- tracing::error!("toggle settings failed {:?}", e);
234
+ Reset(all, completion) => {
235
+ // Element-level reset (symmetric with `save`, which fans out
236
+ // over all panels): drop the element-level global-filter SET
237
+ // first — otherwise a detail panel re-applies it right after
238
+ // the reset — then reset EVERY panel. Master/detail ROLES are
239
+ // layout state (like the panel arrangement), not panel
240
+ // config, so they survive. The `Completion`
241
+ // resolves only after ALL panels' reset runs complete
242
+ // (invariant I6 — previously it rode the active panel only,
243
+ // and the other panels' runs were unowned).
244
+ let workspace = &ctx.props().workspace;
245
+ let origins = workspace.clear_global_filters();
246
+ clear_master_selections(workspace, origins);
247
+ let mut tasks = Vec::new();
248
+ for (id, panel) in workspace
249
+ .panel_ids()
250
+ .into_iter()
251
+ .filter_map(|id| workspace.panel(&id).map(|p| (id, p)))
252
+ {
253
+ stamp_global_overlay(workspace, &id, &panel.session);
254
+ tasks.push(reset_all(
255
+ &panel.session,
256
+ &panel.renderer,
257
+ &ctx.props().presentation,
258
+ all,
259
+ ));
305
260
  }
306
261
 
307
- false
308
- },
309
- ToggleSettingsComplete(_, resolve) => {
310
- ctx.props().presentation.set_open_column_settings(None);
311
- self.on_rendered = Some(resolve);
312
- true
313
- },
314
- OpenColumnSettings {
315
- locator,
316
- sender,
317
- toggle,
318
- } => {
319
- let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
320
- if locator == open_column_settings.locator {
321
- if toggle {
322
- ctx.props().presentation.set_open_column_settings(None);
323
- }
324
- } else {
325
- open_column_settings.locator.clone_from(&locator);
326
- open_column_settings.tab =
327
- if matches!(locator, Some(ColumnLocator::NewExpression)) {
328
- Some(ColumnSettingsTab::Attributes)
329
- } else {
330
- locator.as_ref().and_then(|x| {
331
- x.name().map(|x| {
332
- if self.session_props.is_column_active(x) {
333
- ColumnSettingsTab::Style
334
- } else {
335
- ColumnSettingsTab::Attributes
336
- }
337
- })
338
- })
339
- };
340
-
341
- ctx.props()
342
- .presentation
343
- .set_open_column_settings(Some(open_column_settings));
344
-
345
- if locator.is_some() {
346
- self.settings_panel_selected_tab = SelectedTab::Query;
347
- }
262
+ let run = async move {
263
+ futures::future::join_all(tasks)
264
+ .await
265
+ .into_iter()
266
+ .collect::<ApiResult<Vec<_>>>()?;
267
+ Ok(())
268
+ };
269
+
270
+ match completion {
271
+ Some(completion) => completion.resolve_after(run),
272
+ None => spawn_owned("reset", run),
348
273
  }
349
274
 
350
- if let Some(sender) = sender {
351
- sender.send(()).unwrap();
352
- }
353
-
354
- true
355
- },
356
- SettingsPanelSizeUpdate(Some(x)) => {
357
- self.settings_panel_width_override = Some(x);
358
275
  false
359
276
  },
360
- SettingsPanelSizeUpdate(None) => {
361
- self.settings_panel_width_override = None;
362
- self.settings_panel_auto_width = 0.0;
363
- self.on_settings_panel_dimensions_reset.emit(());
364
- true
365
- },
366
- SettingsPanelTabChanged(tab) => {
367
- let changed = tab != self.settings_panel_selected_tab;
368
- self.settings_panel_selected_tab = tab;
369
- changed
370
- },
371
- SettingsPanelAutoWidth(w) => {
372
- if w > self.settings_panel_auto_width {
373
- self.settings_panel_auto_width = w;
374
- true
375
- } else {
376
- false
277
+ ResetPanel(id, all, sender) => {
278
+ // Panel-scoped reset (toolbar / context menu / `resetPanel()`
279
+ // API): reset ONLY the target panel's config. The
280
+ // element-level cross-filter overlay is workspace state, not
281
+ // panel config, so it's deliberately left in place — the
282
+ // rebuilt view re-applies it via `effective_view_config`,
283
+ // keeping a detail panel consistent with the rest of the
284
+ // workspace. A dangling id no-ops (dropping `sender` rejects
285
+ // the API promise).
286
+ let id = id.map(crate::workspace::PanelId::from);
287
+ if let Some(panel) = ctx.props().workspace.panel_or_active(id.as_ref()) {
288
+ let run = reset_all(
289
+ &panel.session,
290
+ &panel.renderer,
291
+ &ctx.props().presentation,
292
+ all,
293
+ );
294
+
295
+ match sender {
296
+ Some(completion) => completion.resolve_after(run),
297
+ None => spawn_owned("reset-panel", run),
298
+ }
377
299
  }
378
- },
379
- ColumnSettingsPanelSizeUpdate(Some(x)) => {
380
- self.column_settings_panel_width_override = Some(x);
381
- false
382
- },
383
- ColumnSettingsPanelSizeUpdate(None) => {
384
- self.column_settings_panel_width_override = None;
300
+
385
301
  false
386
302
  },
387
- ColumnSettingsTabChanged(tab) => {
388
- let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
389
- open_column_settings.tab.clone_from(&Some(tab));
390
- ctx.props()
391
- .presentation
392
- .set_open_column_settings(Some(open_column_settings));
393
- true
394
- },
395
- ToggleDebug => {
396
- self.debug_open = !self.debug_open;
397
- clone!(ctx.props().renderer, ctx.props().session);
398
- ApiFuture::spawn(async move {
399
- renderer.draw(session.validate().await?.create_view()).await
400
- });
401
-
402
- true
403
- },
404
- UpdateSession(props) => {
405
- let changed = *props != self.session_props;
406
- self.session_props = *props;
407
- changed
408
- },
409
- UpdateSessionStats(stats, has_table) => {
410
- let changed =
411
- stats != self.session_props.stats || has_table != self.session_props.has_table;
412
- self.session_props.stats = stats;
413
- self.session_props.has_table = has_table;
414
- changed
415
- },
416
- UpdateRenderer(props) => {
417
- let changed = *props != self.renderer_props;
418
- self.renderer_props = *props;
419
- changed
420
- },
421
- UpdatePresentation(props) => {
422
- let changed = *props != self.presentation_props;
423
- self.presentation_props = *props;
424
- changed
425
- },
426
- UpdateSettingsOpen(open) => {
427
- let changed = open != self.presentation_props.is_settings_open;
428
- self.presentation_props.is_settings_open = open;
429
- changed
430
- },
431
- UpdateIsWorkspace(is_workspace) => {
432
- let changed = is_workspace != self.presentation_props.is_workspace;
433
- self.presentation_props.is_workspace = is_workspace;
434
- changed
435
- },
436
- UpdateColumnSettings(ocs) => {
437
- let changed = *ocs != self.presentation_props.open_column_settings;
438
- self.presentation_props.open_column_settings = *ocs;
439
- changed
440
- },
441
- UpdateDragDrop(props) => {
442
- let changed = *props != self.dragdrop_props;
443
- self.dragdrop_props = *props;
444
- changed
303
+
304
+ // Panel lifecycle (`panels.rs`)
305
+ LayoutChanged => self.on_layout_changed(ctx),
306
+ SetActivePanel(id, completion) => self.on_set_active_panel(ctx, id, completion),
307
+ ClosePanel(id, completion) => self.on_close_panel(ctx, id, completion),
308
+ CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
309
+ DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
310
+ NewPanel(id) => self.on_new_panel(ctx, id),
311
+ NewPanelFrom { client, table } => self.on_new_panel_from(ctx, client, table),
312
+
313
+ // Master/detail cross-filter (`filters.rs`)
314
+ ToggleMaster(id) => self.on_toggle_master(ctx, id),
315
+ MasterContribution(panel, selection) => {
316
+ self.on_master_contribution(ctx, panel, selection)
445
317
  },
446
- IncrementUpdateCount => {
447
- self.update_count = self.update_count.saturating_add(1);
448
- true
318
+ RemoveGlobalFilter(index) => self.on_remove_global_filter(ctx, index),
319
+ ClearGlobalFilters => self.on_clear_global_filters(ctx),
320
+
321
+ // Settings sidebar + divider pump + column settings (`settings.rs`)
322
+ ToggleSettingsInit(update, resolve) => {
323
+ self.on_toggle_settings_init(ctx, update, resolve)
449
324
  },
450
- DecrementUpdateCount => {
451
- self.update_count = self.update_count.saturating_sub(1);
452
- true
325
+ ToggleSettingsComplete(update, resolve) => {
326
+ self.on_toggle_settings_complete(ctx, update, resolve)
453
327
  },
328
+ SettingsPanelSizeUpdate(x) => self.on_settings_panel_size_update(x),
329
+ SettingsDividerMove(w) => self.on_settings_divider_move(ctx, w),
330
+ SettingsDividerPump => self.on_settings_divider_pump(ctx),
331
+ SettingsDividerCommit(w) => self.on_settings_divider_commit(ctx, w),
332
+ SettingsDividerFinish => self.on_settings_divider_finish(ctx),
333
+ SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
334
+ SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(w),
335
+ OpenColumnSettings {
336
+ locator,
337
+ sender,
338
+ toggle,
339
+ } => self.on_open_column_settings(ctx, locator, sender, toggle),
340
+ ColumnSettingsPanelSizeUpdate(x) => self.on_column_settings_panel_size_update(x),
341
+ ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(w),
342
+ ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(),
343
+ ColumnSettingsTabChanged(tab) => self.on_column_settings_tab_changed(ctx, tab),
344
+ ToggleDebug => self.on_toggle_debug(ctx),
345
+
346
+ // Value-semantic snapshot plumbing (`snapshots.rs`)
347
+ UpdateSession(props) => self.on_update_session(*props),
348
+ UpdateSessionStats(stats, has_table) => self.on_update_session_stats(stats, has_table),
349
+ UpdateGlobalFilters => self.on_update_global_filters(ctx),
350
+ UpdateRenderer(props) => self.on_update_renderer(*props),
351
+ UpdatePresentation(props) => self.on_update_presentation(ctx, *props),
352
+ UpdateSettingsOpen(open) => self.on_update_settings_open(open),
353
+ UpdateIsWorkspace(is_workspace) => self.on_update_is_workspace(is_workspace),
354
+ UpdateColumnSettings(ocs) => self.on_update_column_settings(*ocs),
355
+ UpdateDragDrop(props) => self.on_update_dragdrop(*props),
356
+ UpdateInFlight(count) => self.on_update_in_flight(count),
454
357
  }
455
358
  }
456
359
 
@@ -463,449 +366,35 @@ impl Component for PerspectiveViewer {
463
366
 
464
367
  /// On rendered call notify_resize(). This also triggers any registered
465
368
  /// async callbacks to the Custom Element API.
466
- fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
467
- if self.on_rendered.is_some()
369
+ fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
370
+ // Attach the master/detail selection listeners once (the host element
371
+ // is stable for this component's lifetime).
372
+ if !self.master_listeners_attached {
373
+ let _ = ctx.props().elem.add_event_listener_with_callback(
374
+ "perspective-global-filter",
375
+ self._global_filter_listener.as_ref().unchecked_ref(),
376
+ );
377
+ let _ = ctx.props().elem.add_event_listener_with_callback(
378
+ "perspective-click",
379
+ self._master_click_listener.as_ref().unchecked_ref(),
380
+ );
381
+ self.master_listeners_attached = true;
382
+ }
383
+
384
+ if !self.on_rendered.is_empty()
468
385
  && matches!(self.fonts.get_status(), FontLoaderStatus::Finished)
469
- && self.on_rendered.take().unwrap().send(()).is_err()
470
386
  {
471
- tracing::warn!("Orphan render");
387
+ for resolve in self.on_rendered.drain(..) {
388
+ if resolve.send(()).is_err() {
389
+ tracing::warn!("Orphan render");
390
+ }
391
+ }
472
392
  }
473
393
  }
474
394
 
475
395
  fn view(&self, ctx: &Context<Self>) -> Html {
476
- let Self::Properties {
477
- presentation,
478
- renderer,
479
- session,
480
- ..
481
- } = ctx.props();
482
-
483
- let is_settings_open = self.settings_open
484
- && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
485
-
486
- let mut class = classes!();
487
- if !is_settings_open {
488
- class.push("settings-closed");
489
- }
490
-
491
- if self.session_props.title.is_some() {
492
- class.push("titled");
493
- }
494
-
495
- let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
496
- locator: c,
497
- sender: None,
498
- toggle: true,
499
- });
500
-
501
- let on_split_panel_resize = ctx
502
- .link()
503
- .callback(|(x, _)| SettingsPanelSizeUpdate(Some(x)));
504
-
505
- let on_column_settings_panel_resize = ctx
506
- .link()
507
- .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
508
-
509
- let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
510
- let on_debug = ctx.link().callback(|_| ToggleDebug);
511
- let selected_column = get_current_column_locator(
512
- &self.presentation_props.open_column_settings,
513
- &ctx.props().renderer,
514
- &self.session_props.config,
515
- &self.session_props.metadata,
516
- );
517
-
518
- let selected_tab = self.presentation_props.open_column_settings.tab;
519
- let plugin_name = self.renderer_props.plugin_name.clone();
520
- let available_plugins = self.renderer_props.available_plugins.clone();
521
- let has_table = self.session_props.has_table.clone();
522
- let named_column_count = self.renderer_props.config.config_column_names.len();
523
-
524
- let view_config = self.session_props.config.clone();
525
- let drag_column = self.dragdrop_props.column.clone();
526
- let metadata = self.session_props.metadata.clone();
527
- let on_select_tab = ctx.link().callback(SettingsPanelTabChanged);
528
- let on_auto_width = ctx.link().callback(SettingsPanelAutoWidth);
529
- let settings_panel = html! {
530
- if is_settings_open {
531
- <SettingsPanel
532
- on_close={on_close_settings}
533
- on_resize={&self.on_resize}
534
- on_select_column={on_open_expr_panel}
535
- is_debug={self.debug_open}
536
- {on_debug}
537
- {plugin_name}
538
- {available_plugins}
539
- {has_table}
540
- {named_column_count}
541
- {view_config}
542
- plugin_config={self.renderer_props.plugin_config.clone()}
543
- {drag_column}
544
- metadata={metadata.clone()}
545
- open_column_settings={self.presentation_props.open_column_settings.clone()}
546
- selected_theme={self.presentation_props.selected_theme.clone()}
547
- selected_tab={self.settings_panel_selected_tab}
548
- auto_width={self.settings_panel_auto_width}
549
- on_dimensions_reset={&self.on_settings_panel_dimensions_reset}
550
- {on_select_tab}
551
- {on_auto_width}
552
- {presentation}
553
- {renderer}
554
- {session}
555
- />
556
- }
557
- };
558
-
559
- let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
560
- let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
561
- let column_settings_panel = html! {
562
- if let Some(selected_column) = selected_column {
563
- <SplitPanel
564
- id="modal_panel"
565
- reverse=true
566
- initial_size={self.column_settings_panel_width_override}
567
- on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
568
- on_resize={on_column_settings_panel_resize}
569
- >
570
- <ColumnSettingsPanel
571
- {selected_column}
572
- {selected_tab}
573
- on_close={self.on_close_column_settings.clone()}
574
- width_override={self.column_settings_panel_width_override}
575
- {on_select_tab}
576
- plugin_name={self.renderer_props.plugin_name.clone()}
577
- {metadata}
578
- view_config={self.session_props.config.clone()}
579
- column_stats={self.session_props.column_stats.clone()}
580
- selected_theme={self.presentation_props.selected_theme.clone()}
581
- {presentation}
582
- {renderer}
583
- {session}
584
- />
585
- <></>
586
- </SplitPanel>
587
- }
588
- };
589
-
590
- let on_reset = ctx.link().callback(|all| Reset(all, None));
591
- let is_settings_open = self.settings_open
592
- && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
593
- let main_panel = html! {
594
- <MainPanel
595
- {on_settings}
596
- {on_reset}
597
- session_props={self.session_props.clone()}
598
- renderer_props={self.renderer_props.clone()}
599
- presentation_props={self.presentation_props.clone()}
600
- {is_settings_open}
601
- update_count={self.update_count}
602
- {presentation}
603
- {renderer}
604
- {session}
605
- />
606
- };
607
-
608
- html! {
609
- <StyleProvider root={ctx.props().elem.clone()}>
610
- <div id="component_container">
611
- if is_settings_open {
612
- <SplitPanel
613
- id="app_panel"
614
- reverse=true
615
- skip_empty=true
616
- initial_size={self.settings_panel_width_override}
617
- on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
618
- on_resize={{
619
- let size_cb = on_split_panel_resize.clone();
620
- let resize_cb = resize_callback(&ctx.props().session, &ctx.props().renderer);
621
- move |x| {
622
- size_cb.emit(x);
623
- resize_cb.emit(());
624
- }
625
- }}
626
- on_resize_finished={resize_callback(&ctx.props().session, &ctx.props().renderer)}
627
- >
628
- { settings_panel }
629
- <div id="main_column_container">
630
- { main_panel }
631
- { column_settings_panel }
632
- </div>
633
- </SplitPanel>
634
- } else {
635
- <div id="main_column_container">
636
- { main_panel }
637
- { column_settings_panel }
638
- </div>
639
- }
640
- </div>
641
- <FontLoader ..self.fonts.clone() />
642
- </StyleProvider>
643
- }
396
+ self.render(ctx)
644
397
  }
645
398
 
646
399
  fn destroy(&mut self, _ctx: &Context<Self>) {}
647
400
  }
648
-
649
- impl PerspectiveViewer {
650
- /// Toggle the settings, or force the settings panel either open (true) or
651
- /// closed (false) explicitly. In order to reduce apparent
652
- /// screen-shear, `toggle_settings()` uses a somewhat complex render
653
- /// order: it first resize the plugin's `<div>` without moving it,
654
- /// using `overflow: hidden` to hide the extra draw area; then,
655
- /// after the _async_ drawing of the plugin is complete, it will send a
656
- /// message to complete the toggle action and re-render the element with
657
- /// the settings removed.
658
- ///
659
- /// # Arguments
660
- /// * `force` - Whether to explicitly set the settings panel state to
661
- /// Open/Close (`Some(true)`/`Some(false)`), or to just toggle the current
662
- /// state (`None`).
663
- fn init_toggle_settings_task(
664
- &mut self,
665
- ctx: &Context<Self>,
666
- force: Option<bool>,
667
- sender: Option<Sender<ApiResult<JsValue>>>,
668
- ) {
669
- let is_open = ctx.props().presentation.is_settings_open();
670
- ctx.props().presentation.set_settings_before_open(!is_open);
671
- match force {
672
- Some(force) if is_open == force => {
673
- if let Some(sender) = sender {
674
- sender.send(Ok(JsValue::UNDEFINED)).unwrap();
675
- }
676
- },
677
- Some(_) | None => {
678
- let force = !is_open;
679
- let callback = ctx.link().callback(move |resolve| {
680
- let update = SettingsUpdate::Update(force);
681
- ToggleSettingsComplete(update, resolve)
682
- });
683
-
684
- clone!(
685
- ctx.props().renderer,
686
- ctx.props().session,
687
- ctx.props().presentation
688
- );
689
-
690
- ApiFuture::spawn(async move {
691
- let result = if session.js_get_table().is_some() {
692
- renderer
693
- .presize(force, {
694
- let (sender, receiver) = channel::<()>();
695
- async move {
696
- callback.emit(sender);
697
- presentation.set_settings_open(!is_open);
698
- Ok(receiver.await?)
699
- }
700
- })
701
- .await
702
- } else {
703
- let (sender, receiver) = channel::<()>();
704
- callback.emit(sender);
705
- presentation.set_settings_open(!is_open);
706
- receiver.await?;
707
- Ok(JsValue::UNDEFINED)
708
- };
709
-
710
- if let Some(sender) = sender {
711
- let msg = result.ignore_view_delete();
712
- sender
713
- .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
714
- .into_apierror()?;
715
- };
716
-
717
- Ok(JsValue::undefined())
718
- });
719
- },
720
- };
721
- }
722
- }
723
-
724
- /// Subscribe to PubSub events that still have non-root subscribers and
725
- /// therefore cannot yet be replaced with direct callbacks.
726
- fn create_subscriptions(ctx: &Context<PerspectiveViewer>) -> Vec<Subscription> {
727
- let session_props_sub = {
728
- let session = ctx.props().session.clone();
729
- let cb = ctx
730
- .link()
731
- .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
732
-
733
- let s = &ctx.props().session;
734
- let sub1 = s.table_loaded.add_notify_listener(&cb);
735
- let sub2 = s.table_unloaded.add_notify_listener(&cb);
736
- let sub3 = s.view_created.add_notify_listener(&cb);
737
- let sub4 = s.view_config_changed.add_notify_listener(&cb);
738
- let sub5 = s.title_changed.add_notify_listener(&cb);
739
- let sub6 = s
740
- .view_config_changed
741
- .add_listener(ctx.link().callback(|_| IncrementUpdateCount));
742
-
743
- let sub7 = s
744
- .view_created
745
- .add_listener(ctx.link().callback(|_| DecrementUpdateCount));
746
-
747
- // Stats fetch resolution (populates session.column_stats) triggers
748
- // a fresh `SessionProps` so `column_stats` reaches downstream
749
- // components and the StyleTab re-queries the schema with the
750
- // new value.
751
- let sub8 = s.column_stats_changed.add_notify_listener(&cb);
752
-
753
- vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7, sub8]
754
- };
755
-
756
- let renderer_props_sub = {
757
- let renderer = ctx.props().renderer.clone();
758
- let cb_plugin = ctx.link().callback({
759
- let renderer = renderer.clone();
760
- move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
761
- });
762
-
763
- // Re-snapshot RendererProps when the plugin_config bucket
764
- // changes (in-tab edit via `send_plugin_config`, JSON paste via
765
- // `restore_and_render`, full clear via `reset_all` with
766
- // `all=true`). Without this, `RendererProps.plugin_config`
767
- // would stay frozen at its construct-time value and `PluginTab`
768
- // would render stale.
769
- let cb_plugin_config = ctx.link().callback({
770
- let renderer = renderer.clone();
771
- move |_: serde_json::Map<String, serde_json::Value>| {
772
- UpdateRenderer(Box::new(renderer.to_props(None)))
773
- }
774
- });
775
-
776
- let sub1 = ctx.props().renderer.plugin_changed.add_listener(cb_plugin);
777
- let sub2 = ctx
778
- .props()
779
- .renderer
780
- .plugin_config_changed
781
- .add_listener(cb_plugin_config);
782
-
783
- vec![sub1, sub2]
784
- };
785
-
786
- let presentation_props_sub = {
787
- let presentation = ctx.props().presentation.clone();
788
- let cb_settings = ctx.link().callback(UpdateSettingsOpen);
789
- let cb_theme = {
790
- let pres = presentation.clone();
791
- ctx.link()
792
- .callback(move |(themes, _): (PtrEqRc<Vec<String>>, _)| {
793
- UpdatePresentation(Box::new(pres.to_props(themes)))
794
- })
795
- };
796
-
797
- let cb_column_settings = {
798
- let pres = presentation.clone();
799
- ctx.link().callback(move |_: (bool, Option<String>)| {
800
- UpdateColumnSettings(Box::new(pres.get_open_column_settings()))
801
- })
802
- };
803
-
804
- let sub1 = presentation.settings_open_changed.add_listener(cb_settings);
805
- let sub2 = presentation.theme_config_updated.add_listener(cb_theme);
806
- let sub3 = presentation
807
- .column_settings_open_changed
808
- .add_listener(cb_column_settings);
809
-
810
- vec![sub1, sub2, sub3]
811
- };
812
-
813
- let dragdrop_props_sub = {
814
- let cb_clear = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
815
- let sub1 = ctx
816
- .props()
817
- .presentation
818
- .drop_received
819
- .add_notify_listener(&cb_clear);
820
-
821
- vec![sub1]
822
- };
823
-
824
- let mut subscriptions = Vec::new();
825
- subscriptions.extend(session_props_sub);
826
- subscriptions.extend(renderer_props_sub);
827
- subscriptions.extend(presentation_props_sub);
828
- subscriptions.extend(dragdrop_props_sub);
829
- subscriptions
830
- }
831
-
832
- /// Inject direct callbacks into the engine handles, replacing PubSub fields
833
- /// that were exclusively consumed by the root component.
834
- fn inject_engine_callbacks(ctx: &Context<PerspectiveViewer>) {
835
- // Session: on_stats_changed
836
- {
837
- let session = ctx.props().session.clone();
838
- let cb = ctx.link().callback(move |_: ()| {
839
- UpdateSessionStats(session.get_table_stats(), session.has_table())
840
- });
841
-
842
- *ctx.props().session.on_stats_changed.borrow_mut() = Some(cb);
843
- }
844
-
845
- // Session: on_table_errored
846
- {
847
- let session = ctx.props().session.clone();
848
- let cb = ctx
849
- .link()
850
- .callback(move |_: ()| UpdateSession(Box::new(session.to_props())));
851
-
852
- *ctx.props().session.on_table_errored.borrow_mut() = Some(cb);
853
- }
854
-
855
- // Renderer: on_render_limits_changed (combines UpdateRenderer + column
856
- // locator recheck that were previously two separate PubSub subscriptions).
857
- {
858
- clone!(
859
- ctx.props().presentation,
860
- ctx.props().renderer,
861
- ctx.props().session
862
- );
863
-
864
- let cb = ctx.link().batch_callback(move |limits: RenderLimits| {
865
- let mut msgs = vec![UpdateRenderer(Box::new(renderer.to_props(Some(limits))))];
866
- if !limits.is_update {
867
- let locator = get_current_column_locator(
868
- &presentation.get_open_column_settings(),
869
- &renderer,
870
- &session.get_view_config(),
871
- &session.metadata(),
872
- );
873
-
874
- msgs.push(OpenColumnSettings {
875
- locator,
876
- sender: None,
877
- toggle: false,
878
- });
879
- }
880
-
881
- msgs
882
- });
883
-
884
- *ctx.props().renderer.on_render_limits_changed.borrow_mut() = Some(cb);
885
- }
886
-
887
- // Presentation: on_is_workspace_changed
888
- {
889
- let cb = ctx.link().callback(UpdateIsWorkspace);
890
- *ctx.props()
891
- .presentation
892
- .on_is_workspace_changed
893
- .borrow_mut() = Some(cb);
894
- }
895
-
896
- // Drag/drop: on_dragstart (post-merge: lives on Presentation)
897
- {
898
- let presentation = ctx.props().presentation.clone();
899
- let cb = ctx.link().callback(move |_: DragEffect| {
900
- UpdateDragDrop(Box::new(presentation.drag_drop_props()))
901
- });
902
-
903
- *ctx.props().presentation.on_dragstart.borrow_mut() = Some(cb);
904
- }
905
-
906
- // Drag/drop: on_dragend
907
- {
908
- let cb = ctx.link().callback(|_: ()| UpdateDragDrop(Box::default()));
909
- *ctx.props().presentation.on_dragend.borrow_mut() = Some(cb);
910
- }
911
- }