@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,324 @@
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
+ //! Presize/resize sweeps over the [`Workspace`]'s visible panels, plus the DOM
14
+ //! geometry measurements they (and `MainPanel`'s `before-resize` presize)
15
+ //! depend on. These implement the presize-everywhere architecture: every
16
+ //! geometry change renders each plugin at its *target* box before the layout
17
+ //! commits, so content never lags its container.
18
+
19
+ use std::future::Future;
20
+
21
+ use futures::channel::oneshot::channel;
22
+ use futures::future::join_all;
23
+ use perspective_js::utils::*;
24
+ use wasm_bindgen::JsCast;
25
+
26
+ use crate::workspace::Workspace;
27
+
28
+ /// Resize every *visible* panel's plugin to its current cell box, concurrently.
29
+ /// Used after a layout change that does NOT emit `regular-layout-before-resize`
30
+ /// — settings-panel toggle (the outer SplitPanel) and maximize/minimize — so
31
+ /// the per-panel `ResizeObserver` (now scoped to drags) isn't needed to catch
32
+ /// them. Hidden panels (e.g. an unselected stacked tab → null `offset_parent`)
33
+ /// are skipped; they resize when revealed. `resize()` debounces per renderer.
34
+ pub async fn resize_visible_panels(workspace: &Workspace) {
35
+ let panels = workspace
36
+ .panels()
37
+ .into_iter()
38
+ .filter(|panel| {
39
+ // `offset_parent` is `None` for a `display:none` cell.
40
+ panel
41
+ .renderer
42
+ .active_plugin()
43
+ .and_then(|plugin| {
44
+ plugin
45
+ .unchecked_ref::<web_sys::HtmlElement>()
46
+ .offset_parent()
47
+ })
48
+ .is_some()
49
+ })
50
+ .collect::<Vec<_>>();
51
+
52
+ join_all(panels.iter().map(|p| p.renderer.resize())).await;
53
+ }
54
+
55
+ /// The present closures collected from a presize sweep — the deferred
56
+ /// "reveal" half of the staged-presize protocol
57
+ /// ([`crate::renderer::Renderer::presize_with_dimensions`]). Call
58
+ /// [`Self::reveal`] in the same task as the layout commit the sweep
59
+ /// anticipated, so every panel's staged pixels and the geometry change
60
+ /// reach the screen in a single paint. The reveal runs through `Drop`, so
61
+ /// an error or early-return path between sweep and commit releases every
62
+ /// hold instead of stranding the displays — the pairing invariant is
63
+ /// carried by the value, not by convention.
64
+ #[derive(Default)]
65
+ pub struct StagedPresents(Vec<js_sys::Function>);
66
+
67
+ impl StagedPresents {
68
+ /// Reveal every staged frame now (a semantically-named drop; the
69
+ /// closures are lock-free blits, safe in any task).
70
+ pub fn reveal(self) {}
71
+ }
72
+
73
+ impl From<Vec<js_sys::Function>> for StagedPresents {
74
+ fn from(presents: Vec<js_sys::Function>) -> Self {
75
+ Self(presents)
76
+ }
77
+ }
78
+
79
+ impl Drop for StagedPresents {
80
+ fn drop(&mut self) {
81
+ for f in self.0.drain(..) {
82
+ let _ = f.call0(&wasm_bindgen::JsValue::NULL);
83
+ }
84
+ }
85
+ }
86
+
87
+ /// The `(width, height)` factors each plugin's cell grows by when the settings
88
+ /// pane collapses. The plugin area (`#main_panel_container`) grows to the full
89
+ /// `#layout_area` width (the side settings pane is gone) AND to the full
90
+ /// `#main_column` height — because the status bar switches to
91
+ /// `position:absolute` when settings closes (see `status-bar.css`) and no
92
+ /// longer consumes column height. Each factor defaults to `1.0` if it can't be
93
+ /// measured.
94
+ fn settings_close_grow_ratios(elem: &web_sys::HtmlElement) -> (f64, f64) {
95
+ let measure = |sel: &str| -> Option<web_sys::DomRect> {
96
+ elem.shadow_root()?
97
+ .query_selector(sel)
98
+ .ok()?
99
+ .map(|e| e.get_bounding_client_rect())
100
+ };
101
+
102
+ fn ratio(full: Option<f64>, current: Option<f64>) -> f64 {
103
+ match (full, current) {
104
+ (Some(f), Some(c)) if c > 0.0 && f > c => f / c,
105
+ _ => 1.0,
106
+ }
107
+ }
108
+
109
+ let mpc = measure("#main_panel_container");
110
+ let width = ratio(
111
+ measure("#layout_area").map(|r| r.width()),
112
+ mpc.as_ref().map(|r| r.width()),
113
+ );
114
+ let height = ratio(
115
+ measure("#main_column").map(|r| r.height()),
116
+ mpc.as_ref().map(|r| r.height()),
117
+ );
118
+ (width, height)
119
+ }
120
+
121
+ /// Pre-size each visible plugin to its GROWN post-close cell *before* the
122
+ /// settings pane collapses, so the pane collapses into an already-correct
123
+ /// plugin — one clean resize instead of grow-then-resize. The cell grows in
124
+ /// BOTH width (the side settings pane is gone) and height (the status bar goes
125
+ /// floating).
126
+ pub async fn presize_visible_panels_grown(
127
+ workspace: &Workspace,
128
+ elem: &web_sys::HtmlElement,
129
+ ) -> StagedPresents {
130
+ let (width_ratio, height_ratio) = settings_close_grow_ratios(elem);
131
+ presize_visible_panels_scaled(workspace, elem, width_ratio, height_ratio).await
132
+ }
133
+
134
+ /// Pre-size each visible plugin to its SHRUNK post-open cell *before* the
135
+ /// settings pane mounts (P2), using the open-state geometry deltas cached at
136
+ /// the last close (`(layout_area.w − mpc.w, main_column.h − mpc.h)` — the pane
137
+ /// and divider width and the docked status-bar height, both stable across a
138
+ /// close/open cycle since the pane width persists in the override).
139
+ pub async fn presize_visible_panels_open(
140
+ workspace: &Workspace,
141
+ elem: &web_sys::HtmlElement,
142
+ delta_w: f64,
143
+ delta_h: f64,
144
+ ) -> StagedPresents {
145
+ let Some(mpc) = shadow_rect(elem, "#main_panel_container") else {
146
+ return StagedPresents::default();
147
+ };
148
+
149
+ let (mpc_w0, mpc_h0) = (mpc.width(), mpc.height());
150
+ if mpc_w0 <= 0.0 || mpc_h0 <= 0.0 {
151
+ return StagedPresents::default();
152
+ }
153
+
154
+ let width_ratio = (mpc_w0 - delta_w) / mpc_w0;
155
+ let height_ratio = (mpc_h0 - delta_h) / mpc_h0;
156
+ presize_visible_panels_scaled(workspace, elem, width_ratio, height_ratio).await
157
+ }
158
+
159
+ /// Pre-size each visible plugin for a proposed settings-pane width (P1, the
160
+ /// deferred divider's pump). The grid distributes width proportionally, so the
161
+ /// target cells scale by the container's width ratio, derived arithmetically:
162
+ /// `mpc₁ = mpc₀ + (pane₀ − pane₁)`. Height is unaffected by the divider.
163
+ pub async fn presize_visible_panels_pane_width(
164
+ workspace: &Workspace,
165
+ elem: &web_sys::HtmlElement,
166
+ pane_width: f64,
167
+ ) -> StagedPresents {
168
+ let Some(mpc) = shadow_rect(elem, "#main_panel_container") else {
169
+ return StagedPresents::default();
170
+ };
171
+
172
+ let Some(pane) = shadow_rect(elem, "#app_panel > .split-panel-child") else {
173
+ return StagedPresents::default();
174
+ };
175
+
176
+ let mpc_w0 = mpc.width();
177
+ if mpc_w0 <= 0.0 {
178
+ return StagedPresents::default();
179
+ }
180
+
181
+ let width_ratio = (mpc_w0 + (pane.width() - pane_width)) / mpc_w0;
182
+ presize_visible_panels_scaled(workspace, elem, width_ratio, 1.0).await
183
+ }
184
+
185
+ /// Shared presize core: render every visible plugin at its current cell box
186
+ /// scaled by `(width_ratio, height_ratio)` (track space), before the container
187
+ /// change those ratios anticipate.
188
+ async fn presize_visible_panels_scaled(
189
+ workspace: &Workspace,
190
+ elem: &web_sys::HtmlElement,
191
+ width_ratio: f64,
192
+ height_ratio: f64,
193
+ ) -> StagedPresents {
194
+ if !width_ratio.is_finite()
195
+ || !height_ratio.is_finite()
196
+ || width_ratio <= 0.0
197
+ || height_ratio <= 0.0
198
+ {
199
+ return StagedPresents::default();
200
+ }
201
+
202
+ let mut last_chrome: Option<(f64, f64)> = None;
203
+ let targets = workspace
204
+ .panels()
205
+ .into_iter()
206
+ .filter_map(|panel| {
207
+ let plugin = panel.renderer.active_plugin()?;
208
+ let el = plugin.unchecked_ref::<web_sys::Element>();
209
+ el.unchecked_ref::<web_sys::HtmlElement>().offset_parent()?; // skip hidden
210
+ let plugin_box = el.get_bounding_client_rect();
211
+ let chrome = elem
212
+ .shadow_root()
213
+ .and_then(|r| {
214
+ r.query_selector(&format!(
215
+ "regular-layout-frame[name=\"{}\"]",
216
+ panel.id.as_str()
217
+ ))
218
+ .ok()
219
+ .flatten()
220
+ })
221
+ .and_then(|frame| plugin_chrome(&frame, el));
222
+
223
+ last_chrome = chrome.or(last_chrome);
224
+ let (cw, ch) = last_chrome.unwrap_or(super::CHROME_FALLBACK);
225
+ let w = ((plugin_box.width() + cw) * width_ratio - cw).max(0.0);
226
+ let h = ((plugin_box.height() + ch) * height_ratio - ch).max(0.0);
227
+ Some((panel, w, h))
228
+ })
229
+ .collect::<Vec<_>>();
230
+
231
+ staged_presents_with_deadline(
232
+ workspace.effects().guard(),
233
+ async move {
234
+ join_all(
235
+ targets
236
+ .iter()
237
+ .map(|(p, w, h)| p.renderer.presize_with_dimensions(*w, *h)),
238
+ )
239
+ .await
240
+ .into_iter()
241
+ .filter_map(|r| r.ok().flatten())
242
+ .collect()
243
+ },
244
+ || {},
245
+ )
246
+ .await
247
+ }
248
+
249
+ /// Run `work` (a presize sweep producing staged present closures) against
250
+ /// the [`super::STAGING_DEADLINE_MS`] deadline. When `work` wins, its
251
+ /// presents are returned for the caller to reveal in its commit task. When
252
+ /// the deadline wins, an empty [`StagedPresents`] is returned (the caller's
253
+ /// commit proceeds); `work` then finishes late — `late` runs its cleanup and
254
+ /// the presents are revealed immediately, so a hold can never outlive its
255
+ /// prepare. The single consumer makes a double release inexpressible.
256
+ pub(crate) async fn staged_presents_with_deadline(
257
+ effect: crate::utils::EffectGuard,
258
+ work: impl Future<Output = Vec<js_sys::Function>> + 'static,
259
+ late: impl FnOnce() + 'static,
260
+ ) -> StagedPresents {
261
+ let (done, on_done) = channel::<Vec<js_sys::Function>>();
262
+ ApiFuture::spawn(async move {
263
+ let _effect = effect;
264
+ let presents = work.await;
265
+ if let Err(presents) = done.send(presents) {
266
+ late();
267
+ StagedPresents::from(presents).reveal();
268
+ }
269
+
270
+ Ok(())
271
+ });
272
+
273
+ let deadline = crate::utils::set_timeout(super::STAGING_DEADLINE_MS);
274
+ match futures::future::select(Box::pin(on_done), Box::pin(deadline)).await {
275
+ futures::future::Either::Left((Ok(presents), _)) => StagedPresents::from(presents),
276
+ _ => StagedPresents::default(),
277
+ }
278
+ }
279
+
280
+ /// Measure a shadow-DOM descendant's border-box rect.
281
+ fn shadow_rect(elem: &web_sys::HtmlElement, sel: &str) -> Option<web_sys::DomRect> {
282
+ Some(
283
+ elem.shadow_root()?
284
+ .query_selector(sel)
285
+ .ok()??
286
+ .get_bounding_client_rect(),
287
+ )
288
+ }
289
+
290
+ /// The open-state geometry deltas cached for the next settings *open* (P2):
291
+ /// `(layout_area.w − mpc.w, main_column.h − mpc.h)`. Only meaningful while the
292
+ /// settings pane is open (i.e. measured at close time).
293
+ pub fn measure_settings_open_deltas(elem: &web_sys::HtmlElement) -> Option<(f64, f64)> {
294
+ let mpc = shadow_rect(elem, "#main_panel_container")?;
295
+ let layout_area = shadow_rect(elem, "#layout_area")?;
296
+ let main_column = shadow_rect(elem, "#main_column")?;
297
+ Some((
298
+ (layout_area.width() - mpc.width()).max(0.0),
299
+ (main_column.height() - mpc.height()).max(0.0),
300
+ ))
301
+ }
302
+
303
+ /// The frame chrome a plugin's grid cell has that the plugin itself doesn't
304
+ /// occupy, as `(width, height)` px: margin + border + titlebar. Measured live
305
+ /// from a `regular-layout-frame` and its plugin — robust to theme/CSS changes —
306
+ /// rather than hardcoded constants. `real_coordinates` reports the grid TRACK,
307
+ /// which equals the frame's *margin box* (`getBoundingClientRect` + margins);
308
+ /// the plugin fills the frame's container below its titlebar, so `track −
309
+ /// plugin` is exactly the chrome. Returns `None` if it can't be measured.
310
+ pub fn plugin_chrome(frame: &web_sys::Element, plugin: &web_sys::Element) -> Option<(f64, f64)> {
311
+ let style = web_sys::window()?.get_computed_style(frame).ok()??;
312
+ let px = |prop: &str| {
313
+ style
314
+ .get_property_value(prop)
315
+ .ok()
316
+ .and_then(|v| v.trim().trim_end_matches("px").parse::<f64>().ok())
317
+ .unwrap_or(0.0)
318
+ };
319
+ let frame_box = frame.get_bounding_client_rect();
320
+ let plugin_box = plugin.get_bounding_client_rect();
321
+ let width = frame_box.width() + px("margin-left") + px("margin-right") - plugin_box.width();
322
+ let height = frame_box.height() + px("margin-top") + px("margin-bottom") - plugin_box.height();
323
+ Some((width.max(0.0), height.max(0.0)))
324
+ }
@@ -14,10 +14,10 @@
14
14
  //! presentation columns config / theme, then delegate to `restore_and_render`
15
15
  //! to switch back to the default plugin and redraw.
16
16
 
17
- use futures::channel::oneshot;
18
17
  use perspective_client::clone;
19
18
  use perspective_js::utils::ApiFuture;
20
19
 
20
+ use super::pipeline::RunOrigin;
21
21
  use super::restore_and_render;
22
22
  use crate::config::{
23
23
  ColumnConfigUpdate, OptionalUpdate, PluginConfigUpdate, PluginUpdate, ViewerConfigUpdate,
@@ -32,8 +32,10 @@ use crate::session::{ResetOptions, Session};
32
32
  /// per-column style maps.
33
33
  /// - `all = true`: also clears expressions, per-column styles, and theme.
34
34
  ///
35
- /// Optionally signals `sender` once the reset+redraw round-trip completes,
36
- /// then emits `renderer.reset_changed`.
35
+ /// Returns the reset+redraw round-trip's future — the CALLER owns completion
36
+ /// (invariant I6: message handlers resolve their `Completion` only from run
37
+ /// futures like this one) and error, rather than this task spawning unowned
38
+ /// work.
37
39
  ///
38
40
  /// Delegates plugin selection + draw to [`restore_and_render`], whose
39
41
  /// two-pass restore guarantees the default plugin sees materialized
@@ -45,11 +47,10 @@ pub fn reset_all(
45
47
  renderer: &Renderer,
46
48
  presentation: &Presentation,
47
49
  all: bool,
48
- sender: Option<oneshot::Sender<()>>,
49
- ) {
50
+ ) -> ApiFuture<()> {
50
51
  presentation.set_open_column_settings(None);
51
52
  clone!(session, renderer, presentation);
52
- ApiFuture::spawn(async move {
53
+ ApiFuture::new(async move {
53
54
  session
54
55
  .reset(ResetOptions {
55
56
  config: true,
@@ -60,6 +61,10 @@ pub fn reset_all(
60
61
 
61
62
  presentation.reset_available_themes(None).await;
62
63
  if all {
64
+ // Clear this panel's own per-panel theme so it reverts to inheriting
65
+ // the (reset-to-default) host theme — `reset_theme` only resets the
66
+ // host, which an explicitly-themed panel would otherwise override.
67
+ renderer.set_theme(None);
63
68
  presentation.reset_theme().await?;
64
69
  }
65
70
 
@@ -86,12 +91,17 @@ pub fn reset_all(
86
91
  ..Default::default()
87
92
  };
88
93
 
89
- restore_and_render(&session, &renderer, &presentation, update, async { Ok(()) }).await?;
90
-
91
- if let Some(sender) = sender {
92
- sender.send(()).unwrap();
93
- }
94
-
94
+ // `reset()` is a public element API — `Public` keeps its repaint
95
+ // affordance even on an already-default config.
96
+ restore_and_render(
97
+ &session,
98
+ &renderer,
99
+ &presentation,
100
+ RunOrigin::Public,
101
+ update,
102
+ async { Ok(()) },
103
+ )
104
+ .await?;
95
105
  renderer.reset_changed.emit(());
96
106
  Ok(())
97
107
  })
@@ -10,127 +10,164 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_js::utils::ApiFuture;
13
+ use futures::future::join_all;
14
+ use perspective_client::clone;
15
+ use perspective_js::utils::{ApiFuture, ApiResult};
14
16
  use wasm_bindgen::JsCast;
15
17
  use wasm_bindgen::prelude::*;
16
18
  use web_sys::*;
17
- use yew::prelude::*;
18
19
 
20
+ use super::update_theme::seed_default_themes;
19
21
  use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg};
20
- use crate::js::*;
22
+ use crate::js::{ResizeObserver, ResizeObserverEntry};
21
23
  use crate::presentation::Presentation;
22
- use crate::renderer::*;
24
+ use crate::renderer::Renderer;
23
25
  use crate::root::Root;
24
- use crate::session::{Session, TableLoadState};
25
- use crate::utils::*;
26
+ use crate::session::TableLoadState;
27
+ use crate::workspace::Workspace;
26
28
 
27
- pub struct ResizeObserverHandle {
29
+ struct SizeObserver {
28
30
  elem: HtmlElement,
29
31
  observer: ResizeObserver,
30
32
  _callback: Closure<dyn FnMut(js_sys::Array)>,
31
33
  }
32
34
 
33
- impl ResizeObserverHandle {
34
- pub fn new(
35
- elem: &HtmlElement,
36
- renderer: &Renderer,
37
- session: &Session,
38
- presentation: &Presentation,
39
- root: &Root<PerspectiveViewer>,
40
- ) -> Self {
41
- let on_resize = root
42
- .borrow()
43
- .as_ref()
44
- .unwrap()
45
- .callback(|()| PerspectiveViewerMsg::Resize);
35
+ impl SizeObserver {
36
+ fn new(elem: &HtmlElement, mut on_change: impl FnMut() + 'static) -> Self {
37
+ let mut width = elem.offset_width();
38
+ let mut height = elem.offset_height();
39
+ let target = elem.clone();
40
+ let _callback = Closure::new(move |entries: js_sys::Array| {
41
+ let is_visible = target
42
+ .offset_parent()
43
+ .map(|x| !x.is_null())
44
+ .unwrap_or(false);
45
+ for y in entries.iter() {
46
+ let entry: ResizeObserverEntry = y.unchecked_into();
47
+ let content = entry.content_rect();
48
+ let content_width = content.width().floor() as i32;
49
+ let content_height = content.height().floor() as i32;
50
+ let resized = width != content_width || height != content_height;
51
+ if resized && is_visible {
52
+ on_change();
53
+ }
54
+
55
+ width = content_width;
56
+ height = content_height;
57
+ }
58
+ });
46
59
 
47
- let mut state = ResizeObserverState {
48
- elem: elem.clone(),
49
- renderer: renderer.clone(),
50
- session: session.clone(),
51
- presentation: presentation.clone(),
52
- width: elem.offset_width(),
53
- height: elem.offset_height(),
54
- on_resize,
55
- };
56
-
57
- let _callback = Closure::new(move |xs: js_sys::Array| state.on_resize(&xs));
58
60
  let observer = ResizeObserver::new(_callback.as_ref().unchecked_ref::<js_sys::Function>());
59
61
  observer.observe(elem);
60
62
  Self {
61
63
  elem: elem.clone(),
62
- _callback,
63
64
  observer,
65
+ _callback,
64
66
  }
65
67
  }
66
68
  }
67
69
 
68
- impl Drop for ResizeObserverHandle {
70
+ impl Drop for SizeObserver {
69
71
  fn drop(&mut self) {
70
72
  self.observer.unobserve(&self.elem);
71
73
  }
72
74
  }
73
75
 
74
- #[derive(Clone)]
75
- struct ResizeObserverState {
76
- elem: HtmlElement,
77
- renderer: Renderer,
78
- session: Session,
79
- presentation: Presentation,
80
- width: i32,
81
- height: i32,
82
- on_resize: Callback<()>,
83
- }
76
+ /// The host element's `ResizeObserver`: fans a host box change out to every
77
+ /// panel — a resize for drawn panels, a deferred first render for loaded
78
+ /// panels whose first draw was discarded while invisible.
79
+ pub struct ResizeObserverHandle(#[allow(dead_code)] SizeObserver);
84
80
 
85
- impl ResizeObserverState {
86
- fn on_resize(&mut self, entries: &js_sys::Array) {
87
- let is_visible = self
88
- .elem
89
- .offset_parent()
90
- .map(|x| !x.is_null())
91
- .unwrap_or(false);
92
-
93
- for y in entries.iter() {
94
- let entry: ResizeObserverEntry = y.unchecked_into();
95
- let content = entry.content_rect();
96
- let content_width = content.width().floor() as i32;
97
- let content_height = content.height().floor() as i32;
98
- let resized = self.width != content_width || self.height != content_height;
99
- if resized && is_visible {
100
- let state = self.clone();
101
- clone!(self.on_resize);
102
- ApiFuture::spawn_throttled(async move {
103
- let needs_render = state
104
- .renderer
105
- .clone()
106
- .with_lock(async {
107
- Ok(!state.renderer.is_plugin_activated()?
108
- && matches!(
109
- state.session.has_table(),
110
- Some(TableLoadState::Loaded)
111
- ))
112
- })
113
- .await?;
81
+ impl ResizeObserverHandle {
82
+ pub fn new(
83
+ elem: &HtmlElement,
84
+ workspace: &Workspace,
85
+ presentation: &Presentation,
86
+ root: &Root<PerspectiveViewer>,
87
+ ) -> Self {
88
+ let on_resize = root
89
+ .borrow()
90
+ .as_ref()
91
+ .unwrap()
92
+ .callback(|()| PerspectiveViewerMsg::Resize);
93
+
94
+ clone!(workspace, presentation);
95
+ Self(SizeObserver::new(elem, move || {
96
+ clone!(workspace, presentation, on_resize);
97
+ ApiFuture::spawn_throttled(async move {
98
+ let mut plans = Vec::new();
99
+ for panel in workspace.panels() {
100
+ // Skip only a plugin that HAS DRAWN but is currently
101
+ // hidden (an unselected stacked tab → null
102
+ // `offset_parent`); it renders when revealed.
103
+ // `is_plugin_activated` is an explicit has-drawn flag,
104
+ // so an eagerly-MOUNTED but never-drawn plugin (its
105
+ // deferred first render was discarded by the
106
+ // `is_visible` gate) falls through to the
107
+ // `needs_render` branch below, which owes it a draw.
108
+ if panel.renderer.is_plugin_activated()?
109
+ && let Some(plugin) = panel.renderer.active_plugin()
110
+ && plugin
111
+ .unchecked_ref::<HtmlElement>()
112
+ .offset_parent()
113
+ .is_none()
114
+ {
115
+ continue;
116
+ }
114
117
 
118
+ let needs_render = !panel.renderer.is_plugin_activated()?
119
+ && matches!(panel.session.has_table(), Some(TableLoadState::Loaded));
120
+
121
+ plans.push((panel, needs_render));
122
+ }
123
+
124
+ // For first mounts below: "stamp before restyle" — the
125
+ // plugin captures its `--psp-*` CSS at first draw.
126
+ if plans.iter().any(|(_, needs_render)| *needs_render) {
127
+ presentation.reset_attached();
128
+ seed_default_themes(&presentation, &workspace).await;
129
+ }
130
+
131
+ join_all(plans.into_iter().map(|(panel, needs_render)| async move {
132
+ // A concurrent run (e.g. the `load()` that made
133
+ // `needs_render` true) is harmless: this run queues
134
+ // behind it, reconciles `Unchanged` with no state delta
135
+ // and dispatches nothing — never a duplicate draw.
115
136
  if needs_render {
116
- state.presentation.reset_attached();
117
- super::update_and_render(
118
- &state.session,
119
- &state.renderer,
120
- Default::default(),
121
- )?
122
- .await?;
137
+ super::just_render(&panel.session, &panel.renderer)?.await?;
123
138
  } else {
124
- state.renderer.resize().await?;
139
+ panel.renderer.resize().await?;
125
140
  }
126
141
 
127
- on_resize.emit(());
128
- Ok(())
129
- });
130
- }
142
+ ApiResult::<()>::Ok(())
143
+ }))
144
+ .await
145
+ .into_iter()
146
+ .collect::<ApiResult<Vec<_>>>()?;
131
147
 
132
- self.width = content_width;
133
- self.height = content_height;
134
- }
148
+ on_resize.emit(());
149
+ Ok(())
150
+ });
151
+ }))
152
+ }
153
+ }
154
+
155
+ /// A per-panel [`ResizeObserver`] bound to a single panel's slotted plugin
156
+ /// element, resizing only that panel's [`Renderer`] when its box changes.
157
+ pub struct PanelResizeObserverHandle(#[allow(dead_code)] SizeObserver);
158
+
159
+ impl PanelResizeObserverHandle {
160
+ pub fn new(elem: &HtmlElement, renderer: &Renderer) -> Self {
161
+ clone!(renderer);
162
+ Self(SizeObserver::new(elem, move || {
163
+ clone!(renderer);
164
+ ApiFuture::spawn_throttled(async move {
165
+ if renderer.is_plugin_activated()? {
166
+ renderer.resize().await?;
167
+ }
168
+
169
+ Ok(())
170
+ });
171
+ }))
135
172
  }
136
173
  }