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