@perspective-dev/viewer 4.5.2 → 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 (178) 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.d.ts +9 -0
  18. package/dist/esm/perspective-viewer.inline.js +20 -1
  19. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  20. package/dist/esm/perspective-viewer.js +20 -1
  21. package/dist/esm/perspective-viewer.js.map +4 -4
  22. package/dist/esm/plugin.d.ts +71 -5
  23. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  24. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  25. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  26. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  27. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  28. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  29. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  30. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  32. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  33. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  34. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  35. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  36. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  37. package/dist/wasm/perspective-viewer.d.ts +211 -77
  38. package/dist/wasm/perspective-viewer.js +528 -180
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  41. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  42. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  43. package/package.json +3 -2
  44. package/src/css/_context_menu.css +14 -0
  45. package/src/css/column-settings-panel.css +1 -1
  46. package/src/css/containers/context-menu.css +101 -0
  47. package/src/css/panel-tab.css +193 -0
  48. package/src/css/plugin-selector.css +7 -10
  49. package/src/css/status-bar.css +75 -81
  50. package/src/css/viewer.css +180 -70
  51. package/src/rust/components/column_dropdown.rs +40 -30
  52. package/src/rust/components/column_selector/active_column.rs +7 -18
  53. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  54. package/src/rust/components/column_selector/config_selector.rs +33 -81
  55. package/src/rust/components/column_selector/filter_column.rs +5 -15
  56. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  57. package/src/rust/components/column_selector/sort_column.rs +3 -6
  58. package/src/rust/components/column_selector.rs +5 -11
  59. package/src/rust/components/containers/split_panel.rs +175 -62
  60. package/src/rust/components/context_menu.rs +130 -0
  61. package/src/rust/components/form/debug.rs +3 -0
  62. package/src/rust/components/global_filter_bar.rs +76 -0
  63. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  64. package/src/rust/components/main_panel/msg.rs +62 -0
  65. package/src/rust/components/main_panel/presize.rs +220 -0
  66. package/src/rust/components/main_panel/reconcile.rs +224 -0
  67. package/src/rust/components/main_panel/render.rs +385 -0
  68. package/src/rust/components/main_panel/update.rs +184 -0
  69. package/src/rust/components/main_panel.rs +311 -71
  70. package/src/rust/components/mod.rs +4 -0
  71. package/src/rust/components/panel_menu.rs +486 -0
  72. package/src/rust/components/panel_tab.rs +505 -0
  73. package/src/rust/components/plugin_tab.rs +1 -1
  74. package/src/rust/components/settings_panel.rs +13 -5
  75. package/src/rust/components/status_bar.rs +110 -178
  76. package/src/rust/components/status_indicator.rs +6 -9
  77. package/src/rust/components/style/mod.rs +5 -0
  78. package/src/rust/components/style/style_provider.rs +2 -0
  79. package/src/rust/components/viewer/filters.rs +133 -0
  80. package/src/rust/components/viewer/msg.rs +180 -0
  81. package/src/rust/components/viewer/panels.rs +326 -0
  82. package/src/rust/components/viewer/render.rs +243 -0
  83. package/src/rust/components/viewer/settings.rs +402 -0
  84. package/src/rust/components/viewer/snapshots.rs +134 -0
  85. package/src/rust/components/viewer/wiring.rs +505 -0
  86. package/src/rust/components/viewer.rs +243 -756
  87. package/src/rust/config/export_method.rs +1 -1
  88. package/src/rust/config/mod.rs +4 -0
  89. package/src/rust/config/options.rs +71 -0
  90. package/src/rust/config/viewer_config.rs +24 -2
  91. package/src/rust/config/workspace_config.rs +92 -0
  92. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  93. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  94. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  95. package/src/rust/custom_elements/viewer.rs +1015 -286
  96. package/src/rust/custom_events.rs +212 -61
  97. package/src/rust/js/mod.rs +2 -0
  98. package/src/rust/js/plugin.rs +297 -10
  99. package/src/rust/js/regular_layout.rs +258 -0
  100. package/src/rust/lib.rs +20 -1
  101. package/src/rust/presentation.rs +66 -33
  102. package/src/rust/queries/get_viewer_config.rs +29 -11
  103. package/src/rust/queries/plugin_column_styles.rs +2 -2
  104. package/src/rust/renderer/activate.rs +41 -14
  105. package/src/rust/renderer/dispatch.rs +436 -0
  106. package/src/rust/renderer/plugin_config.rs +495 -0
  107. package/src/rust/renderer/registry.rs +18 -6
  108. package/src/rust/renderer/render_timer.rs +29 -10
  109. package/src/rust/renderer.rs +350 -837
  110. package/src/rust/session/column_defaults_update.rs +26 -8
  111. package/src/rust/session/view_subscription.rs +30 -15
  112. package/src/rust/session.rs +753 -264
  113. package/src/rust/tasks/apply_global_filters.rs +89 -0
  114. package/src/rust/tasks/auto_pause.rs +171 -0
  115. package/src/rust/tasks/copy_export.rs +29 -19
  116. package/src/rust/tasks/create_panel.rs +243 -0
  117. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  118. package/src/rust/tasks/edit_expression.rs +4 -4
  119. package/src/rust/tasks/eject.rs +64 -23
  120. package/src/rust/tasks/mod.rs +28 -4
  121. package/src/rust/tasks/pipeline.rs +411 -0
  122. package/src/rust/tasks/presize_panels.rs +324 -0
  123. package/src/rust/tasks/reset_all.rs +22 -12
  124. package/src/rust/tasks/resize_observer.rs +124 -87
  125. package/src/rust/tasks/restore_and_render.rs +80 -86
  126. package/src/rust/tasks/restore_panel.rs +145 -0
  127. package/src/rust/tasks/send_column_config.rs +5 -2
  128. package/src/rust/tasks/send_plugin_config.rs +5 -2
  129. package/src/rust/tasks/set_edit_mode.rs +59 -0
  130. package/src/rust/tasks/sync_update_panels.rs +152 -0
  131. package/src/rust/tasks/table_lifecycle.rs +158 -0
  132. package/src/rust/tasks/update_theme.rs +56 -12
  133. package/src/rust/utils/completion.rs +56 -0
  134. package/src/rust/utils/custom_element.rs +5 -0
  135. package/src/rust/utils/debounce.rs +130 -7
  136. package/src/rust/utils/mod.rs +6 -0
  137. package/src/rust/utils/pending_effects.rs +62 -0
  138. package/src/rust/utils/spawn.rs +33 -0
  139. package/src/rust/utils/weak_scope.rs +0 -10
  140. package/src/rust/workspace.rs +846 -0
  141. package/src/svg/active-indicator.svg +3 -0
  142. package/src/themes/botanical.css +5 -2
  143. package/src/themes/defaults.css +54 -2
  144. package/src/themes/dracula.css +6 -3
  145. package/src/themes/gruvbox-dark.css +5 -2
  146. package/src/themes/gruvbox.css +5 -2
  147. package/src/themes/icons.css +4 -2
  148. package/src/themes/monokai.css +5 -2
  149. package/src/themes/phosphor.css +5 -2
  150. package/src/themes/pro-dark.css +5 -2
  151. package/src/themes/pro.css +2 -1
  152. package/src/themes/solarized-dark.css +5 -2
  153. package/src/themes/solarized.css +5 -2
  154. package/src/themes/vaporwave.css +5 -2
  155. package/src/ts/bootstrap.ts +8 -7
  156. package/src/ts/extensions.ts +30 -2
  157. package/src/ts/perspective-viewer.ts +10 -0
  158. package/src/ts/plugin.ts +72 -5
  159. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  160. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  161. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  162. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  163. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  164. package/src/ts/ts-rs/Layout.ts +9 -0
  165. package/src/ts/ts-rs/Orientation.ts +6 -0
  166. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  168. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  169. package/src/ts/ts-rs/TabLayout.ts +11 -0
  170. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  171. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  172. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  173. package/src/rust/tasks/intersection_observer.rs +0 -98
  174. package/src/rust/tasks/update_and_render.rs +0 -110
  175. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -10,10 +10,12 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- //! Wire engine PubSub fanout to JavaScript `CustomEvent` dispatch on the host
14
- //! element. [`wire_custom_events`] is the single subscription site; every
15
- //! `perspective-*` `CustomEvent` originates from a PubSub fire on `Session`,
16
- //! `Renderer`, or `Presentation`.
13
+ //! Wire engine PubSub fanout to JavaScript `CustomEvent` dispatch. Split into
14
+ //! [`wire_element_events`] (element-level, `Presentation`-driven, wired once)
15
+ //! and [`wire_panel_events`] (one panel's `Session`/`Renderer`, wired per
16
+ //! panel and owned by the `Panel`). Every `perspective-*` `CustomEvent`
17
+ //! originates from a PubSub fire; per-panel events dispatch on the originating
18
+ //! panel's plugin so `event.target` identifies the panel.
17
19
 
18
20
  use perspective_client::{ViewWindow, clone};
19
21
  use perspective_js::json;
@@ -27,6 +29,7 @@ use crate::queries::get_viewer_config;
27
29
  use crate::renderer::{ColumnConfigMap, Renderer};
28
30
  use crate::session::Session;
29
31
  use crate::utils::{AddListener, Subscription};
32
+ use crate::workspace::Workspace;
30
33
 
31
34
  /// Dispatch a JS `CustomEvent` named `perspective-{name}` on `elem`.
32
35
  fn dispatch_event<T: Into<JsValue>>(elem: &HtmlElement, name: &str, event: T) -> ApiResult<()> {
@@ -41,7 +44,57 @@ fn dispatch_event<T: Into<JsValue>>(elem: &HtmlElement, name: &str, event: T) ->
41
44
  Ok(())
42
45
  }
43
46
 
47
+ /// The event target for a panel's events: the panel's own plugin element (so
48
+ /// `event.target`/`composedPath()` carries its `slot=<panel-id>`, identifying
49
+ /// the originating panel), falling back to the host when no plugin is drawn.
50
+ /// Events dispatched here bubble to the host, so element-level listeners still
51
+ /// fire.
52
+ fn panel_target(elem: &HtmlElement, renderer: &Renderer) -> EventTarget {
53
+ renderer
54
+ .active_plugin()
55
+ .filter(|plugin| elem.contains(Some(plugin.unchecked_ref::<web_sys::Node>())))
56
+ .map(|plugin| plugin.unchecked_into::<EventTarget>())
57
+ .unwrap_or_else(|| elem.clone().unchecked_into())
58
+ }
59
+
60
+ /// Dispatch a `perspective-{name}` `CustomEvent` for a single panel — on the
61
+ /// panel's plugin (see [`panel_target`]), bubbling + composed so it reaches the
62
+ /// host. The originating panel is identifiable from `event.target`; `detail`
63
+ /// shapes are otherwise unchanged from the single-panel case.
64
+ fn dispatch_panel_event(
65
+ elem: &HtmlElement,
66
+ renderer: &Renderer,
67
+ name: &str,
68
+ detail: &JsValue,
69
+ ) -> ApiResult<()> {
70
+ let event_init = web_sys::CustomEventInit::new();
71
+ event_init.set_detail(detail);
72
+ event_init.set_bubbles(true);
73
+ event_init.set_composed(true);
74
+ let event = web_sys::CustomEvent::new_with_event_init_dict(
75
+ format!("perspective-{}", name).as_str(),
76
+ &event_init,
77
+ )?;
78
+
79
+ panel_target(elem, renderer).dispatch_event(&event)?;
80
+ Ok(())
81
+ }
82
+
83
+ /// Annotate an object `detail` with its originating panel id (`detail.panel`),
84
+ /// for the events whose detail is a plain JSON object (`select`,
85
+ /// `column-style-change`). Additive — existing single-panel listeners reading
86
+ /// the prior fields are unaffected; skipped when `detail` isn't an object (e.g.
87
+ /// a null selection) or the panel has no slot.
88
+ fn annotate_panel(detail: &JsValue, renderer: &Renderer) {
89
+ if detail.is_object()
90
+ && let Some(id) = renderer.slot_name()
91
+ {
92
+ let _ = js_sys::Reflect::set(detail, &JsValue::from_str("panel"), &JsValue::from_str(&id));
93
+ }
94
+ }
95
+
44
96
  fn dispatch_column_settings_open_changed(
97
+ workspace: &Workspace,
45
98
  elem: &HtmlElement,
46
99
  open: bool,
47
100
  column_name: Option<String>,
@@ -50,28 +103,47 @@ fn dispatch_column_settings_open_changed(
50
103
  event_init.set_detail(&JsValue::from(
51
104
  json!({"open": open, "column_name": column_name}),
52
105
  ));
106
+ // Bubble so the event still reaches the host (and any public listeners) even
107
+ // though it's dispatched on a panel's plugin below it.
108
+ event_init.set_bubbles(true);
53
109
  let event = web_sys::CustomEvent::new_with_event_init_dict(
54
110
  "perspective-toggle-column-settings",
55
111
  &event_init,
56
- );
112
+ )
113
+ .unwrap();
57
114
 
58
- elem.dispatch_event(&event.unwrap()).unwrap();
59
- }
115
+ // Dispatch on the ACTIVE panel's plugin, not the shared host. The merged
116
+ // viewer hosts every panel's plugin under one element, and each datagrid
117
+ // listens for this event on itself — so dispatching on the host (which the
118
+ // event bubbles to) would highlight EVERY datagrid. The column settings
119
+ // apply to the active panel, so scope the highlight to its plugin; fall back
120
+ // to the host if none is drawn.
121
+ let target: web_sys::EventTarget = workspace
122
+ .active_id()
123
+ .and_then(|id| workspace.panel(&id))
124
+ .and_then(|panel| panel.renderer.active_plugin())
125
+ .map(|plugin| plugin.unchecked_into())
126
+ .unwrap_or_else(|| elem.clone().unchecked_into());
60
127
 
61
- fn dispatch_plugin_changed(elem: &HtmlElement, plugin: &JsPerspectiveViewerPlugin) {
62
- let event_init = web_sys::CustomEventInit::new();
63
- event_init.set_detail(plugin);
64
- let event =
65
- web_sys::CustomEvent::new_with_event_init_dict("perspective-plugin-update", &event_init);
128
+ target.dispatch_event(&event).unwrap();
129
+ }
66
130
 
67
- elem.dispatch_event(&event.unwrap()).unwrap();
131
+ fn dispatch_plugin_changed(
132
+ elem: &HtmlElement,
133
+ renderer: &Renderer,
134
+ plugin: &JsPerspectiveViewerPlugin,
135
+ ) {
136
+ // Detail is the plugin element itself (identity is via `event.target`, the
137
+ // panel's plugin — see `panel_target`).
138
+ let _ = dispatch_panel_event(elem, renderer, "plugin-update", plugin.as_ref());
68
139
  }
69
140
 
70
- /// Per-element memoized config-change dispatcher. Reads/writes the dedup
71
- /// cell on `presentation.last_dispatched_config` so each viewer instance
72
- /// has its own cache — without this, a second viewer reloading the same
73
- /// table the first viewer used would have its initial `config-update`
74
- /// suppressed.
141
+ /// Per-panel memoized config-change dispatcher. The dedup cell now lives on the
142
+ /// `Session` (per panel) rather than the `Presentation` (per element), so N
143
+ /// panels don't cross-suppress each other's `config-update`s. The event is
144
+ /// dispatched on the panel's plugin (bubbling to the host), so `event.target`
145
+ /// identifies the panel; `detail` remains the bare [`ViewerConfig`] — NOT
146
+ /// wrapped — so `viewer.restore(e.detail)` round-trips unchanged.
75
147
  fn dispatch_config_update(
76
148
  elem: &HtmlElement,
77
149
  session: &Session,
@@ -80,47 +152,77 @@ fn dispatch_config_update(
80
152
  ) {
81
153
  clone!(session, renderer, presentation);
82
154
  let elem = elem.clone();
83
- perspective_js::utils::ApiFuture::spawn(async move {
84
- let viewer_config = get_viewer_config(&session, &renderer, &presentation).await?;
155
+ let tracker = session.clone();
156
+ tracker.track_dispatch(async move {
157
+ let viewer_config = renderer
158
+ .clone()
159
+ .with_lock(async { get_viewer_config(&session, &renderer, &presentation).await })
160
+ .await?;
161
+
85
162
  if viewer_config.view_config != Default::default()
86
- && Some(&viewer_config) != presentation.last_dispatched_config.borrow().as_ref()
163
+ && Some(&viewer_config) != session.last_dispatched_config.borrow().as_deref()
87
164
  {
88
- let json_config = JsValue::from_serde_ext(&viewer_config)?;
165
+ let viewer_config = std::rc::Rc::new(viewer_config);
166
+ *session.last_dispatched_config.borrow_mut() = Some(viewer_config.clone());
167
+ let memo = std::cell::RefCell::new(None);
168
+ let get_config = Closure::<dyn Fn() -> Result<JsValue, JsValue>>::new(move || {
169
+ let mut memo = memo.borrow_mut();
170
+ if memo.is_none() {
171
+ *memo = Some(JsValue::from_serde_ext(&*viewer_config).map_err(JsValue::from)?);
172
+ }
173
+
174
+ Ok(memo.as_ref().unwrap().clone())
175
+ });
176
+
177
+ let detail = js_sys::Object::new();
178
+ js_sys::Reflect::set(
179
+ &detail,
180
+ &JsValue::from_str("getConfig"),
181
+ get_config.as_ref(),
182
+ )?;
89
183
  let event_init = web_sys::CustomEventInit::new();
90
- event_init.set_detail(&json_config);
184
+ event_init.set_detail(&detail);
185
+ event_init.set_bubbles(true);
186
+ event_init.set_composed(true);
91
187
  let event = web_sys::CustomEvent::new_with_event_init_dict(
92
188
  "perspective-config-update",
93
189
  &event_init,
94
- );
190
+ )?;
95
191
 
96
- *presentation.last_dispatched_config.borrow_mut() = Some(viewer_config);
97
- elem.dispatch_event(&event.unwrap()).unwrap();
192
+ panel_target(&elem, &renderer).dispatch_event(&event)?;
193
+ drop(get_config);
98
194
  }
99
195
 
100
196
  Ok(())
101
197
  });
102
198
  }
103
199
 
104
- /// Wire PubSub channels on `session`, `renderer`, and `presentation` to the
105
- /// `perspective-*` `CustomEvent` set on `elem`. The returned
106
- /// `Vec<Subscription>` must be kept alive for the lifetime of the element;
107
- /// dropping it detaches all listeners.
108
- pub fn wire_custom_events(
200
+ /// Wire the ELEMENT-level PubSub channels (`Presentation`-driven, not tied to
201
+ /// any one panel) to the `perspective-*` `CustomEvent` set on `elem`. Wired
202
+ /// ONCE for the element's lifetime. The config-update-bearing events
203
+ /// (theme/settings) emit for the **active** panel, resolved from `workspace`
204
+ /// at fire time. The returned `Vec<Subscription>` must outlive the element.
205
+ pub fn wire_element_events(
109
206
  elem: &HtmlElement,
110
- session: &Session,
111
- renderer: &Renderer,
112
207
  presentation: &Presentation,
208
+ workspace: &Workspace,
113
209
  ) -> Vec<Subscription> {
114
210
  let theme_sub = presentation.theme_config_updated.add_listener({
115
- clone!(elem, session, renderer, presentation);
116
- move |_| dispatch_config_update(&elem, &session, &renderer, &presentation)
211
+ clone!(elem, presentation, workspace);
212
+ move |_| {
213
+ if let Some(panel) = workspace.active_panel() {
214
+ dispatch_config_update(&elem, &panel.session, &panel.renderer, &presentation);
215
+ }
216
+ }
117
217
  });
118
218
 
119
219
  let settings_sub = presentation.settings_open_changed.add_listener({
120
- clone!(elem, session, renderer, presentation);
220
+ clone!(elem, presentation, workspace);
121
221
  move |open: bool| {
122
222
  dispatch_event(&elem, "toggle-settings", open).unwrap();
123
- dispatch_config_update(&elem, &session, &renderer, &presentation);
223
+ if let Some(panel) = workspace.active_panel() {
224
+ dispatch_config_update(&elem, &panel.session, &panel.renderer, &presentation);
225
+ }
124
226
  }
125
227
  });
126
228
 
@@ -132,22 +234,73 @@ pub fn wire_custom_events(
132
234
  });
133
235
 
134
236
  let column_settings_sub = presentation.column_settings_open_changed.add_listener({
135
- clone!(elem);
237
+ clone!(elem, workspace);
136
238
  move |(open, column_name)| {
137
- dispatch_column_settings_open_changed(&elem, open, column_name);
239
+ dispatch_column_settings_open_changed(&workspace, &elem, open, column_name);
138
240
  // column_settings is ethereal; do not change the config
139
241
  }
140
242
  });
141
243
 
244
+ let statusbar_ptr_sub = presentation.statusbar_pointer_event.add_listener({
245
+ clone!(elem);
246
+ move |event: PointerEvent| {
247
+ dispatch_event(&elem, &format!("statusbar-{}", event.type_()), &event).unwrap();
248
+ }
249
+ });
250
+
251
+ // The element-level global (cross-) filter set changed — masters'
252
+ // selections, chip × / "Clear", role toggles, restore. `detail` is the
253
+ // flattened `Vec<Filter>`; deliberately NOT `perspective-config-update`
254
+ // (these filters are invisible to every panel's config by design).
255
+ let global_filters_sub = workspace.filters_changed().add_listener({
256
+ clone!(elem, workspace);
257
+ move |_: ()| {
258
+ let filters =
259
+ JsValue::from_serde_ext(&workspace.global_filters()).expect("serializable filters");
260
+ dispatch_event(&elem, "global-filter-update", filters).unwrap();
261
+ }
262
+ });
263
+
264
+ vec![
265
+ theme_sub,
266
+ before_settings_sub,
267
+ settings_sub,
268
+ column_settings_sub,
269
+ statusbar_ptr_sub,
270
+ global_filters_sub,
271
+ ]
272
+ }
273
+
274
+ /// Wire ONE panel's `session`/`renderer` PubSub channels to the
275
+ /// `perspective-*` `CustomEvent` set, dispatched on that panel's plugin (so
276
+ /// `event.target` identifies the panel — see [`dispatch_panel_event`]). Called
277
+ /// per panel at creation; the returned subscriptions are owned by the `Panel`
278
+ /// and drop when it is removed, so every panel (seed, `addPanel`, restored)
279
+ /// fires its own events and non-active panels are no longer silent.
280
+ ///
281
+ /// Needs no `Workspace`: each handler acts on its own `session`/`renderer`, and
282
+ /// the panel id for annotation is read from `renderer.slot_name()` at fire
283
+ /// time.
284
+ pub fn wire_panel_events(
285
+ elem: &HtmlElement,
286
+ session: &Session,
287
+ renderer: &Renderer,
288
+ presentation: &Presentation,
289
+ ) -> Vec<Subscription> {
142
290
  let plugin_sub = renderer.plugin_changed.add_listener({
143
291
  clone!(elem, session, renderer, presentation);
144
292
  move |plugin| {
145
- dispatch_plugin_changed(&elem, &plugin);
293
+ dispatch_plugin_changed(&elem, &renderer, &plugin);
146
294
  dispatch_config_update(&elem, &session, &renderer, &presentation);
147
295
  }
148
296
  });
149
297
 
150
- let view_sub = session.view_created.add_listener({
298
+ // `commit_reconciled` (NOT `view_created`): the config-update event must
299
+ // fire for every reconciled commit — including SKIP/REUSE/paused binds
300
+ // that construct no `View` — or `flush()` has nothing to join (the
301
+ // paused-`load()` case). The dispatcher dedups, so REBUILD's pairing
302
+ // with `view_created` can't double-fire it.
303
+ let view_sub = session.commit_reconciled.add_listener({
151
304
  clone!(elem, session, renderer, presentation);
152
305
  move |_| dispatch_config_update(&elem, &session, &renderer, &presentation)
153
306
  });
@@ -158,13 +311,14 @@ pub fn wire_custom_events(
158
311
  });
159
312
 
160
313
  let unload_sub = session.table_unloaded.add_listener({
161
- clone!(elem);
314
+ clone!(elem, renderer);
162
315
  move |x: bool| {
163
- if !x {
164
- dispatch_event(&elem, "table-delete-before", JsValue::UNDEFINED).unwrap();
316
+ let name = if !x {
317
+ "table-delete-before"
165
318
  } else {
166
- dispatch_event(&elem, "table-delete", JsValue::UNDEFINED).unwrap()
167
- }
319
+ "table-delete"
320
+ };
321
+ let _ = dispatch_panel_event(&elem, &renderer, name, &JsValue::UNDEFINED);
168
322
  }
169
323
  });
170
324
 
@@ -172,7 +326,8 @@ pub fn wire_custom_events(
172
326
  clone!(elem, session, renderer, presentation);
173
327
  move |window: Option<ViewWindow>| {
174
328
  let detail = JsValue::from_serde_ext(&window).unwrap();
175
- dispatch_event(&elem, "select", &detail).unwrap();
329
+ annotate_panel(&detail, &renderer);
330
+ let _ = dispatch_panel_event(&elem, &renderer, "select", &detail);
176
331
  dispatch_config_update(&elem, &session, &renderer, &presentation);
177
332
  }
178
333
  });
@@ -181,28 +336,25 @@ pub fn wire_custom_events(
181
336
  clone!(elem, session, renderer, presentation);
182
337
  move |cfg: ColumnConfigMap| {
183
338
  let detail = JsValue::from_serde_ext(&cfg).unwrap();
184
- dispatch_event(&elem, "column-style-change", &detail).unwrap();
339
+ annotate_panel(&detail, &renderer);
340
+ let _ = dispatch_panel_event(&elem, &renderer, "column-style-change", &detail);
185
341
  dispatch_config_update(&elem, &session, &renderer, &presentation);
186
342
  }
187
343
  });
188
344
 
189
345
  let status_click_sub = session.status_indicator_clicked.add_listener({
190
- clone!(elem);
191
- move |_| dispatch_event(&elem, "status-indicator-click", JsValue::UNDEFINED).unwrap()
192
- });
193
-
194
- let statusbar_ptr_sub = presentation.statusbar_pointer_event.add_listener({
195
- clone!(elem);
196
- move |event: PointerEvent| {
197
- dispatch_event(&elem, &format!("statusbar-{}", event.type_()), &event).unwrap();
346
+ clone!(elem, renderer);
347
+ move |_| {
348
+ let _ = dispatch_panel_event(
349
+ &elem,
350
+ &renderer,
351
+ "status-indicator-click",
352
+ &JsValue::UNDEFINED,
353
+ );
198
354
  }
199
355
  });
200
356
 
201
357
  vec![
202
- theme_sub,
203
- before_settings_sub,
204
- settings_sub,
205
- column_settings_sub,
206
358
  plugin_sub,
207
359
  view_sub,
208
360
  title_sub,
@@ -210,6 +362,5 @@ pub fn wire_custom_events(
210
362
  select_sub,
211
363
  column_style_sub,
212
364
  status_click_sub,
213
- statusbar_ptr_sub,
214
365
  ]
215
366
  }
@@ -19,10 +19,12 @@ pub mod clipboard_item;
19
19
  mod intersection_observer;
20
20
  mod mimetype;
21
21
  pub mod plugin;
22
+ pub mod regular_layout;
22
23
  pub mod resize_observer;
23
24
 
24
25
  pub use self::clipboard::*;
25
26
  pub use self::intersection_observer::*;
26
27
  pub use self::mimetype::*;
27
28
  pub use self::plugin::*;
29
+ pub use self::regular_layout::*;
28
30
  pub use self::resize_observer::*;