@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
@@ -0,0 +1,184 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! The small `MainPanelMsg` handlers dispatched from `MainPanel::update` —
14
+ //! pointer forwarding, layout-close detection, tab/active sync, and the panel
15
+ //! context menu. The heavier `BeforeResize` presize lives in
16
+ //! [`super::presize`].
17
+
18
+ use perspective_js::utils::JsValueSerdeExt;
19
+ use wasm_bindgen::prelude::*;
20
+ use yew::prelude::*;
21
+
22
+ use super::MainPanel;
23
+ use crate::components::panel_menu::PanelCommand;
24
+ use crate::js::{Layout, RegularLayout};
25
+ use crate::tasks::resize_callback;
26
+
27
+ impl MainPanel {
28
+ pub(super) fn on_pointer_event(
29
+ &self,
30
+ ctx: &Context<Self>,
31
+ event: web_sys::PointerEvent,
32
+ ) -> bool {
33
+ if event.target().map(JsValue::from)
34
+ == self
35
+ .main_panel_ref
36
+ .cast::<web_sys::HtmlElement>()
37
+ .map(JsValue::from)
38
+ {
39
+ ctx.props().presentation.statusbar_pointer_event.emit(event);
40
+ }
41
+
42
+ false
43
+ }
44
+
45
+ pub(super) fn on_layout_updated(&mut self, ctx: &Context<Self>) -> bool {
46
+ // A panel still tracked in `inserted` but no longer present in
47
+ // the layout (calculatePath → null) was removed by the layout —
48
+ // i.e. its frame's close button was pressed. Report it for
49
+ // disposal.
50
+ let Some(el) = self.layout_ref.cast::<web_sys::HtmlElement>() else {
51
+ return false;
52
+ };
53
+
54
+ // A `regular-layout-update` ends any in-flight drag (it fires on
55
+ // the drop, not during the drag). Drop the drag ResizeObserver(s)
56
+ // bound in `BeforeResize`; the post-commit `resize_callback` loop
57
+ // below resizes the dropped panel to its final cell.
58
+ self.panel_resize_observers.clear();
59
+
60
+ let layout: RegularLayout = el.unchecked_into();
61
+ let mut closed = Vec::new();
62
+ self.inserted.retain(|id| {
63
+ if layout.contains_panel(id) {
64
+ true
65
+ } else {
66
+ closed.push(id.clone());
67
+ false
68
+ }
69
+ });
70
+
71
+ for id in closed {
72
+ ctx.props().on_close_panel.emit(id);
73
+ }
74
+
75
+ // A layout change (divider drag, insert, restore) reflows the
76
+ // grid cells without resizing the host, so the host
77
+ // `ResizeObserver` never fires. Resize every activated panel's
78
+ // plugin to match its new cell size — same per-panel
79
+ // `resize_callback` the insert path uses (idempotent, and each
80
+ // `Renderer` debounces its own resize).
81
+ for id in ctx.props().panel_ids.iter() {
82
+ // Skip any panel regular-layout reports as absent — it's
83
+ // leaving the layout (a close / drag-out in flight). Spawning
84
+ // a resize draw for it would race its disposal, and spawned
85
+ // tasks can't be cancelled. App-initiated closes already drop
86
+ // the panel from `panel_ids` before this fires; this also
87
+ // covers a layout-originated removal.
88
+ if !layout.contains_panel(id.as_str()) {
89
+ continue;
90
+ }
91
+
92
+ if let Some(panel) = ctx.props().workspace.panel(id)
93
+ && panel.renderer.is_plugin_activated().unwrap_or(false)
94
+ {
95
+ resize_callback(&panel.session, &panel.renderer).emit(());
96
+ }
97
+ }
98
+
99
+ // Recompute which panels are hidden behind an unselected stack
100
+ // index. Selection changes route through `restore` and so also
101
+ // fire `regular-layout-update` — this is the single point of
102
+ // truth for tab visibility. Re-render only when it changed, so
103
+ // each `PanelTab` re-syncs its `visible` class.
104
+ let hidden = layout
105
+ .save()
106
+ .into_serde_ext::<Layout>()
107
+ .map(|tree| tree.hidden_slot_names().into_iter().collect())
108
+ .unwrap_or_default();
109
+
110
+ if hidden != self.hidden_tabs {
111
+ self.hidden_tabs = hidden;
112
+ true
113
+ } else {
114
+ false
115
+ }
116
+ }
117
+
118
+ pub(super) fn on_tab_selected(&self, ctx: &Context<Self>, name: String) -> bool {
119
+ ctx.props().on_activate_panel.emit(name);
120
+ false
121
+ }
122
+
123
+ pub(super) fn on_context_menu(
124
+ &mut self,
125
+ ctx: &Context<Self>,
126
+ id: Option<String>,
127
+ x: f64,
128
+ y: f64,
129
+ ) -> bool {
130
+ // Make the right-clicked panel active so active-targeting
131
+ // commands (e.g. Reset) act on it; then show the menu.
132
+ if let Some(id) = &id {
133
+ ctx.props().on_activate_panel.emit(id.clone());
134
+ }
135
+
136
+ self.context_menu = Some((x, y, id));
137
+ true
138
+ }
139
+
140
+ pub(super) fn on_close_context_menu(&mut self) -> bool {
141
+ self.context_menu = None;
142
+ true
143
+ }
144
+
145
+ pub(super) fn on_command(&mut self, ctx: &Context<Self>, cmd: PanelCommand) -> bool {
146
+ let Some((_, _, target)) = self.context_menu.clone() else {
147
+ return false;
148
+ };
149
+
150
+ match cmd {
151
+ // Maximize/Restore act on the layout element this
152
+ // component owns. As of regular-layout 0.6.1 both route
153
+ // through the presize queue (cancelable `before-resize`
154
+ // with the post-transition paths), so the shared
155
+ // `BeforeResize` gate presizes the now-visible plugin(s)
156
+ // — no reactive post-commit resize sweep needed.
157
+ PanelCommand::Maximize => {
158
+ if let Some(id) = target
159
+ && let Some(el) = self.layout_ref.cast::<web_sys::HtmlElement>()
160
+ {
161
+ el.unchecked_ref::<RegularLayout>().maximize(&id);
162
+ self.maximized = Some(id);
163
+ }
164
+ },
165
+ PanelCommand::Restore => {
166
+ if let Some(el) = self.layout_ref.cast::<web_sys::HtmlElement>() {
167
+ el.unchecked_ref::<RegularLayout>().minimize();
168
+ }
169
+
170
+ self.maximized = None;
171
+ },
172
+ // The stage menu (no target panel) offers ONLY `NewFrom`, whose
173
+ // handler resolves the client/table purely from the loaded-clients
174
+ // registry — the panel id is unused there, so the empty id never
175
+ // reaches a panel lookup.
176
+ cmd => ctx
177
+ .props()
178
+ .on_panel_command
179
+ .emit((target.unwrap_or_default(), cmd)),
180
+ }
181
+
182
+ false
183
+ }
184
+ }
@@ -10,18 +10,46 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ //! `MainPanel`: the multi-panel layout host. Owns the `<regular-layout>`
14
+ //! element, the per-panel `<PanelTab>`s, the shared status bar, and the panel
15
+ //! context menu. The `Component` impl below is thin — its handlers live in the
16
+ //! submodules:
17
+ //!
18
+ //! - [`update`] — the small `MainPanelMsg` handlers (pointer, close detection,
19
+ //! tab/active sync, context menu).
20
+ //! - [`presize`] — the `BeforeResize` pre-size-every-plugin algorithm.
21
+ //! - [`reconcile`] — the `rendered` layout reconcile + per-panel theme stamp.
22
+ //! - [`frame_theme`] — the `rendered` frame-background mirror (each
23
+ //! `<regular-layout-frame>`'s panel-theme background var).
24
+ //! - [`render`] — the `view` (status bar, cells, tabs, menu).
25
+
26
+ mod frame_theme;
27
+ mod presize;
28
+ mod reconcile;
29
+ mod render;
30
+ mod update;
31
+
32
+ pub mod msg;
33
+
34
+ use std::collections::{HashMap, HashSet};
35
+
36
+ use perspective_js::utils::JsValueSerdeExt;
13
37
  use wasm_bindgen::prelude::*;
14
38
  use yew::prelude::*;
15
39
 
16
- use super::render_warning::RenderWarning;
17
- use super::status_bar::StatusBar;
40
+ pub use self::msg::MainPanelMsg;
41
+ use super::panel_menu::PanelCommand;
18
42
  use crate::presentation::{Presentation, PresentationProps};
19
43
  use crate::renderer::*;
20
44
  use crate::session::{Session, SessionProps};
21
- use crate::tasks::dismiss_render_warning_callback;
45
+ use crate::tasks::PanelResizeObserverHandle;
46
+ use crate::workspace::{PanelId, Workspace};
22
47
 
23
48
  #[derive(Clone, Properties)]
24
49
  pub struct MainPanelProps {
50
+ /// Toggle the settings sidebar open. Fired by a `PanelTab`'s open-settings
51
+ /// button (after selecting + activating that panel) — the tabs are the
52
+ /// only open-settings affordance.
25
53
  pub on_settings: Callback<()>,
26
54
 
27
55
  /// Reset callback forwarded from the root component. Fired when the user
@@ -29,6 +57,21 @@ pub struct MainPanelProps {
29
57
  /// + column configs), `false` for config-only.
30
58
  pub on_reset: Callback<bool>,
31
59
 
60
+ /// Fired with a panel id when its frame titlebar is pressed, to make it the
61
+ /// active panel.
62
+ pub on_activate_panel: Callback<String>,
63
+
64
+ /// Fired with a panel id when its frame's close button removes it from the
65
+ /// layout, so the root can dispose the panel.
66
+ pub on_close_panel: Callback<String>,
67
+
68
+ /// Fired with `(panel id, command)` when the panel context menu selects a
69
+ /// command the root executes (New/Duplicate/Reset/ToggleMaster/Close).
70
+ /// Maximize/Restore are handled HERE (this component owns the layout
71
+ /// element), and Export/Copy end-to-end by
72
+ /// [`PanelMenu`](super::panel_menu::PanelMenu) itself.
73
+ pub on_panel_command: Callback<(String, PanelCommand)>,
74
+
32
75
  /// Snapshots threaded from root. Read for `has_table`, `title` here in
33
76
  /// the panel itself; threaded wholesale to `StatusBar`/`StatusIndicator`.
34
77
  pub session_props: SessionProps,
@@ -41,7 +84,42 @@ pub struct MainPanelProps {
41
84
  /// Root-managed in-flight render counter (not engine state).
42
85
  pub update_count: u32,
43
86
 
44
- /// State
87
+ /// Ids of every layout panel, in order; one `<regular-layout>` cell is
88
+ /// rendered per id. Drives re-render when panels are added/removed.
89
+ pub panel_ids: Vec<PanelId>,
90
+
91
+ /// `(panel id, session title)` for every panel. Rendered into the
92
+ /// `<regular-layout>` `style` as `--regular-layout-<id>--title` custom
93
+ /// properties, which regular-layout's tabs display via `::before` content.
94
+ pub panel_titles: Vec<(String, Option<String>)>,
95
+
96
+ /// `(panel id, per-panel theme)` for every panel. A snapshot so a
97
+ /// per-panel theme change re-renders MainPanel — `renderer.theme()` is
98
+ /// interior-mutable and not otherwise observed by `eq`. Each frame inlines
99
+ /// its theme's `--psp-*` block only when it diverges from the host theme.
100
+ pub panel_themes: Vec<(String, Option<String>)>,
101
+
102
+ /// The master (filter-source) panel ids, sorted. A snapshot so a master
103
+ /// toggle re-renders MainPanel — the role set is interior-mutable on
104
+ /// `Workspace` and not otherwise observed by `eq`. Drives each tab's
105
+ /// broadcast badge.
106
+ pub panel_masters: Vec<PanelId>,
107
+
108
+ /// Element-level global filters (fed by master/detail selection), threaded
109
+ /// to the `StatusBar` where the global-filter chips are rendered.
110
+ pub global_filters: Vec<perspective_client::config::Filter>,
111
+
112
+ /// Remove the global filter at this index (a chip's × in the `StatusBar`).
113
+ pub on_remove_global_filter: Callback<usize>,
114
+
115
+ /// Clear all global filters (the "Clear" affordance in the `StatusBar`).
116
+ pub on_clear_global_filters: Callback<()>,
117
+
118
+ /// The multi-panel model, for per-panel `Renderer`/`Session` access when
119
+ /// reconciling `insertPanel`/`removePanel`.
120
+ pub workspace: Workspace,
121
+
122
+ /// State (the *active* panel's handles — for the shared status bar).
45
123
  pub session: Session,
46
124
  pub renderer: Renderer,
47
125
  pub presentation: Presentation,
@@ -54,6 +132,15 @@ impl PartialEq for MainPanelProps {
54
132
  && self.presentation_props == rhs.presentation_props
55
133
  && self.is_settings_open == rhs.is_settings_open
56
134
  && self.update_count == rhs.update_count
135
+ && self.panel_ids == rhs.panel_ids
136
+ && self.panel_titles == rhs.panel_titles
137
+ && self.panel_themes == rhs.panel_themes
138
+ && self.panel_masters == rhs.panel_masters
139
+ && self.global_filters == rhs.global_filters
140
+ && self.workspace == rhs.workspace
141
+ && self.session == rhs.session
142
+ && self.renderer == rhs.renderer
143
+ && self.presentation == rhs.presentation
57
144
  }
58
145
  }
59
146
 
@@ -61,99 +148,252 @@ impl MainPanelProps {
61
148
  fn is_title(&self) -> bool {
62
149
  self.session_props.title.is_some()
63
150
  }
64
- }
65
151
 
66
- #[derive(Debug)]
67
- pub enum MainPanelMsg {
68
- PointerEvent(web_sys::PointerEvent),
152
+ pub(super) fn effective_panel_theme(&self, id: &str) -> Option<String> {
153
+ self.panel_themes
154
+ .iter()
155
+ .find(|(pid, _)| pid == id)
156
+ .and_then(|(_, theme)| theme.clone())
157
+ .or_else(|| self.presentation_props.available_themes.first().cloned())
158
+ }
69
159
  }
70
160
 
71
161
  pub struct MainPanel {
72
162
  main_panel_ref: NodeRef,
163
+
164
+ /// Ref to the `<regular-layout>` element hosting the panel cells.
165
+ layout_ref: NodeRef,
166
+
167
+ /// Panel slots currently placed in `layout_ref`'s grid, reconciled against
168
+ /// `panel_ids` on each render so we `insertPanel`/`removePanel` exactly
169
+ /// once per add/remove.
170
+ inserted: Vec<String>,
171
+
172
+ /// `regular-layout-update` listener (close detection); kept alive here and
173
+ /// attached to the layout element once in `rendered`.
174
+ _layout_update_listener: Closure<dyn FnMut(web_sys::Event)>,
175
+
176
+ /// `regular-layout-select` listener (active-panel sync); kept alive here
177
+ /// and attached alongside the update listener.
178
+ _layout_select_listener: Closure<dyn FnMut(web_sys::Event)>,
179
+
180
+ /// `regular-layout-before-resize` listener (presize hook); kept alive here
181
+ /// and attached alongside the others.
182
+ _layout_before_resize_listener: Closure<dyn FnMut(web_sys::Event)>,
183
+
184
+ /// `contextmenu` listener on the panel *container* — one stable attach
185
+ /// point covering the whole stage, independent of the layout reconcile;
186
+ /// at zero panels it opens the stage menu. Kept alive here and attached
187
+ /// once in `rendered`. Imperative — not a Yew `oncontextmenu` — because
188
+ /// the plugin body is light-DOM attached by the renderer (its DOM parent
189
+ /// is the host, not the frame), so Yew's delegated handler never matches
190
+ /// a right-click there. A native listener catches it via composed
191
+ /// bubbling and resolves the panel from the path.
192
+ _contextmenu_listener: Closure<dyn FnMut(web_sys::Event)>,
193
+
194
+ /// The `<regular-layout>` ELEMENT the layout listeners are attached to.
195
+ /// Compared by identity in `reconcile` — if the element is ever a
196
+ /// different instance (it should never be: the render keys it into a
197
+ /// fully-keyed sibling list precisely so Yew reuses it), the listeners
198
+ /// are re-attached and `inserted` is reset so the fresh (empty) layout
199
+ /// is repopulated instead of silently orphaning every panel. A latched
200
+ /// `bool` here once turned an unkeyed-diff element replacement into
201
+ /// "Duplicate commits into an EMPTY tree with no `before-resize`
202
+ /// listener": the old element left the DOM with the committed tree and
203
+ /// every listener, and nothing ever noticed.
204
+ listener_target: Option<web_sys::HtmlElement>,
205
+
206
+ /// Per-panel `ResizeObserver`s, keyed by panel id, each observing that
207
+ /// panel's slotted plugin element and resizing only that panel's
208
+ /// `Renderer`. Bound in `BeforeResize` for the DRAGGED panel only (it is
209
+ /// excluded from the presize paths, and the `overlay` presize is disabled);
210
+ /// dropped on `LayoutUpdated` (the drop).
211
+ panel_resize_observers: HashMap<String, PanelResizeObserverHandle>,
212
+
213
+ /// Panels currently *hidden* behind an unselected index of a tab stack,
214
+ /// recomputed from the layout tree on every `regular-layout-update`.
215
+ /// Drives each tab's `visible` prop (`PanelTab` marks the front tab of
216
+ /// every stack, not just the active panel). Empty until the first layout
217
+ /// update — every panel starts visible.
218
+ hidden_tabs: HashSet<String>,
219
+
220
+ /// Open context menu as `(client x, client y, target panel id)`; outer
221
+ /// `None` when closed. A `None` *panel id* is the empty-stage menu (zero
222
+ /// panels — "New" only). Rendered as a cursor-anchored
223
+ /// [`PanelMenu`](super::panel_menu::PanelMenu) overlay.
224
+ context_menu: Option<(f64, f64, Option<String>)>,
225
+
226
+ /// Id of the currently maximized panel (via `regular-layout.maximize`), or
227
+ /// `None`. Transient (regular-layout doesn't persist it); drives the
228
+ /// Maximize/Restore menu label. Cleared when the panel leaves the layout.
229
+ maximized: Option<String>,
230
+
231
+ /// Theme-name-keyed cache of backgrounds read off stamped plugin
232
+ /// elements, the mirror source for frames whose own plugin is unreadable
233
+ /// (see [`frame_theme`]). Cleared when the theme registry changes.
234
+ theme_backgrounds: HashMap<String, String>,
235
+
236
+ /// The inputs `stamp_frame_themes` last mirrored from; unchanged inputs
237
+ /// skip the pass (and its forced style recalcs) on unrelated re-renders.
238
+ /// `None` until the first *fully-resolved* pass — an unresolved frame
239
+ /// leaves it unlatched so the mirror retries each render.
240
+ stamped_frame_themes: Option<frame_theme::FrameThemeSnapshot>,
241
+
242
+ /// `Workspace::staged_changed` → [`MainPanelMsg::StagedChanged`] on THIS
243
+ /// component's scope (see the message doc for why not the root's).
244
+ _staged_sub: crate::utils::Subscription,
73
245
  }
74
246
 
75
247
  impl Component for MainPanel {
76
248
  type Message = MainPanelMsg;
77
249
  type Properties = MainPanelProps;
78
250
 
79
- fn create(_ctx: &Context<Self>) -> Self {
251
+ fn create(ctx: &Context<Self>) -> Self {
252
+ let cb = ctx.link().callback(|_: ()| MainPanelMsg::LayoutUpdated);
253
+ let listener = Closure::wrap(
254
+ Box::new(move |_: web_sys::Event| cb.emit(())) as Box<dyn FnMut(web_sys::Event)>
255
+ );
256
+
257
+ let select_cb = ctx.link().callback(MainPanelMsg::TabSelected);
258
+ let select_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
259
+ #[derive(serde::Deserialize)]
260
+ struct SelectDetail {
261
+ name: String,
262
+ }
263
+
264
+ if let Some(custom) = event.dyn_ref::<web_sys::CustomEvent>()
265
+ && let Ok(SelectDetail { name }) = custom.detail().into_serde_ext()
266
+ {
267
+ select_cb.emit(name);
268
+ }
269
+ }) as Box<dyn FnMut(web_sys::Event)>);
270
+
271
+ // `preventDefault()` synchronously suspends the layout's resize commit
272
+ // (the event is cancelable); the component then pre-sizes each panel and
273
+ // calls `resumeResize` to release it (see `MainPanelMsg::BeforeResize`).
274
+ let before_resize_cb = ctx.link().callback(MainPanelMsg::BeforeResize);
275
+ let before_resize_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
276
+ event.prevent_default();
277
+ before_resize_cb.emit(event);
278
+ }) as Box<dyn FnMut(web_sys::Event)>);
279
+
280
+ // Imperative `contextmenu` listener: a right-click anywhere in a panel
281
+ // (the plugin body included) opens the panel menu. The plugin body is
282
+ // light-DOM attached by the renderer, so a Yew `oncontextmenu` on the
283
+ // frame never matches it (Yew walks the vdom, not the composed path).
284
+ // This native listener resolves the panel from the
285
+ // `<regular-layout-frame name=…>` on the event's composed path, then
286
+ // suppresses the browser menu and emits. On the EMPTY stage (zero
287
+ // panels — the persistent `<regular-layout>` has no frame
288
+ // descendants), it opens the stage menu instead, whose "New" items
289
+ // create the first panel.
290
+ let contextmenu_cb = ctx
291
+ .link()
292
+ .callback(|(id, x, y)| MainPanelMsg::ContextMenu(id, x, y));
293
+ let contextmenu_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
294
+ let path = event.composed_path();
295
+ let mut panel_id = None;
296
+ for i in 0..path.length() {
297
+ let node = path.get(i);
298
+ if let Some(el) = node.dyn_ref::<web_sys::Element>()
299
+ && el.tag_name().eq_ignore_ascii_case("regular-layout-frame")
300
+ && let Some(name) = el.get_attribute("name")
301
+ {
302
+ panel_id = Some(name);
303
+ break;
304
+ }
305
+ }
306
+
307
+ let is_empty_stage = || {
308
+ event
309
+ .current_target()
310
+ .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
311
+ .is_some_and(|el| {
312
+ el.query_selector("regular-layout-frame")
313
+ .ok()
314
+ .flatten()
315
+ .is_none()
316
+ })
317
+ };
318
+
319
+ if let Some(id) = panel_id {
320
+ event.prevent_default();
321
+ let mouse = event.unchecked_ref::<web_sys::MouseEvent>();
322
+ contextmenu_cb.emit((Some(id), mouse.client_x() as f64, mouse.client_y() as f64));
323
+ } else if is_empty_stage() {
324
+ event.prevent_default();
325
+ let mouse = event.unchecked_ref::<web_sys::MouseEvent>();
326
+ contextmenu_cb.emit((None, mouse.client_x() as f64, mouse.client_y() as f64));
327
+ }
328
+ // With panels present, a click outside every frame lets the
329
+ // native menu through.
330
+ }) as Box<dyn FnMut(web_sys::Event)>);
331
+
332
+ let staged_sub = {
333
+ use crate::utils::AddListener;
334
+ let cb = ctx.link().callback(|_: ()| MainPanelMsg::StagedChanged);
335
+ ctx.props()
336
+ .workspace
337
+ .staged_changed()
338
+ .add_listener(move |()| cb.emit(()))
339
+ };
340
+
80
341
  Self {
81
342
  main_panel_ref: NodeRef::default(),
343
+ layout_ref: NodeRef::default(),
344
+ inserted: Vec::new(),
345
+ _layout_update_listener: listener,
346
+ _layout_select_listener: select_listener,
347
+ _layout_before_resize_listener: before_resize_listener,
348
+ _contextmenu_listener: contextmenu_listener,
349
+ listener_target: None,
350
+ panel_resize_observers: HashMap::new(),
351
+ hidden_tabs: HashSet::new(),
352
+ context_menu: None,
353
+ maximized: None,
354
+ theme_backgrounds: HashMap::new(),
355
+ stamped_frame_themes: None,
356
+ _staged_sub: staged_sub,
82
357
  }
83
358
  }
84
359
 
85
360
  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
86
361
  match msg {
87
- MainPanelMsg::PointerEvent(event) => {
88
- if event.target().map(JsValue::from)
89
- == self
90
- .main_panel_ref
91
- .cast::<web_sys::HtmlElement>()
92
- .map(JsValue::from)
93
- {
94
- ctx.props().presentation.statusbar_pointer_event.emit(event);
95
- }
96
-
97
- false
98
- },
362
+ MainPanelMsg::PointerEvent(event) => self.on_pointer_event(ctx, event),
363
+ MainPanelMsg::StagedChanged => true,
364
+ MainPanelMsg::LayoutUpdated => self.on_layout_updated(ctx),
365
+ MainPanelMsg::TabSelected(name) => self.on_tab_selected(ctx, name),
366
+ MainPanelMsg::ContextMenu(id, x, y) => self.on_context_menu(ctx, id, x, y),
367
+ MainPanelMsg::CloseContextMenu => self.on_close_context_menu(),
368
+ MainPanelMsg::Command(cmd) => self.on_command(ctx, cmd),
369
+ MainPanelMsg::BeforeResize(event) => self.on_before_resize(ctx, event),
99
370
  }
100
371
  }
101
372
 
102
- fn changed(&mut self, _ctx: &Context<Self>, _old: &Self::Properties) -> bool {
103
- true
373
+ fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
374
+ ctx.props() != old
104
375
  }
105
376
 
106
- fn view(&self, ctx: &Context<Self>) -> Html {
107
- let Self::Properties {
108
- presentation,
109
- renderer,
110
- session,
111
- ..
112
- } = ctx.props();
113
-
114
- let is_settings_open = ctx.props().is_settings_open;
115
- let on_settings = (!is_settings_open).then(|| ctx.props().on_settings.clone());
116
-
117
- let mut class = classes!();
118
- if !is_settings_open {
119
- class.push("settings-closed");
377
+ fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
378
+ // The `contextmenu` listener attaches to the panel CONTAINER — one
379
+ // stable attach point covering the whole stage, decoupled from the
380
+ // layout reconcile. Panel right-clicks bubble to it through the
381
+ // layout on the composed path; at zero panels it serves the stage
382
+ // menu.
383
+ if first_render && let Some(el) = self.main_panel_ref.cast::<web_sys::HtmlElement>() {
384
+ let _ = el.add_event_listener_with_callback(
385
+ "contextmenu",
386
+ self._contextmenu_listener.as_ref().unchecked_ref(),
387
+ );
120
388
  }
121
389
 
122
- if ctx.props().is_title() {
123
- class.push("titled");
124
- }
390
+ self.size_staging_wrappers();
391
+ self.reconcile(ctx);
392
+ self.stamp_frame_themes(ctx);
393
+ }
125
394
 
126
- let pointerdown = ctx.link().callback(MainPanelMsg::PointerEvent);
127
- let on_dismiss_warning = dismiss_render_warning_callback(session, renderer);
128
-
129
- html! {
130
- <div id="main_column">
131
- <StatusBar
132
- id="status_bar"
133
- {on_settings}
134
- on_reset={ctx.props().on_reset.clone()}
135
- session_props={ctx.props().session_props.clone()}
136
- presentation_props={ctx.props().presentation_props.clone()}
137
- is_settings_open={ctx.props().is_settings_open}
138
- update_count={ctx.props().update_count}
139
- {presentation}
140
- {renderer}
141
- {session}
142
- />
143
- <div
144
- id="main_panel_container"
145
- ref={self.main_panel_ref.clone()}
146
- {class}
147
- onpointerdown={pointerdown}
148
- >
149
- <RenderWarning
150
- on_dismiss={on_dismiss_warning}
151
- dimensions={ctx.props().renderer_props.render_limits}
152
- />
153
- <slot />
154
- </div>
155
- </div>
156
- }
395
+ fn view(&self, ctx: &Context<Self>) -> Html {
396
+ self.render(ctx)
157
397
  }
158
398
 
159
399
  fn destroy(&mut self, _ctx: &Context<Self>) {}
@@ -18,6 +18,7 @@ pub mod column_dropdown;
18
18
  pub mod column_selector;
19
19
  pub mod column_settings_sidebar;
20
20
  pub mod containers;
21
+ pub mod context_menu;
21
22
  pub mod copy_dropdown;
22
23
  pub mod datetime_column_style;
23
24
  pub mod editable_header;
@@ -28,9 +29,12 @@ pub mod filter_dropdown;
28
29
  pub mod font_loader;
29
30
  pub mod form;
30
31
  pub mod function_dropdown;
32
+ pub mod global_filter_bar;
31
33
  pub mod main_panel;
32
34
  pub mod modal;
33
35
  pub mod number_series_style;
36
+ pub mod panel_menu;
37
+ pub mod panel_tab;
34
38
  pub mod plugin_selector;
35
39
  pub mod plugin_tab;
36
40
  pub mod portal;