@perspective-dev/viewer 4.5.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/esm/extensions.d.ts +22 -3
  24. package/dist/esm/perspective-viewer.d.ts +9 -0
  25. package/dist/esm/perspective-viewer.inline.js +20 -1
  26. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  27. package/dist/esm/perspective-viewer.js +20 -1
  28. package/dist/esm/perspective-viewer.js.map +4 -4
  29. package/dist/esm/plugin.d.ts +71 -5
  30. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  32. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  33. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  34. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  35. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  36. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  37. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  38. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -0
  39. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  40. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  41. package/dist/esm/ts-rs/ViewerConfig.d.ts +8 -2
  42. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
  43. package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
  44. package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
  45. package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
  46. package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
  47. package/dist/esm/ts-rs/Windows.d.ts +10 -0
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  50. package/dist/wasm/perspective-viewer.d.ts +299 -78
  51. package/dist/wasm/perspective-viewer.js +544 -186
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  54. package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
  55. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
  56. package/package.json +3 -2
  57. package/src/css/_context_menu.css +14 -0
  58. package/src/css/column-selector.css +1 -0
  59. package/src/css/column-settings-panel.css +68 -3
  60. package/src/css/column-style.css +4 -3
  61. package/src/css/config-selector.css +22 -1
  62. package/src/css/containers/context-menu.css +107 -0
  63. package/src/css/containers/tabs.css +2 -2
  64. package/src/css/panel-tab.css +193 -0
  65. package/src/css/plugin-selector.css +7 -10
  66. package/src/css/status-bar.css +75 -81
  67. package/src/css/viewer.css +188 -70
  68. package/src/rust/components/column_dropdown.rs +40 -30
  69. package/src/rust/components/column_selector/active_column.rs +28 -45
  70. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  71. package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
  72. package/src/rust/components/column_selector/config_selector.rs +40 -82
  73. package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
  74. package/src/rust/components/column_selector/filter_column.rs +5 -15
  75. package/src/rust/components/column_selector/inactive_column.rs +11 -9
  76. package/src/rust/components/column_selector/sort_column.rs +3 -6
  77. package/src/rust/components/column_selector.rs +25 -14
  78. package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
  79. package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
  80. package/src/rust/components/column_settings_sidebar.rs +163 -16
  81. package/src/rust/components/containers/dragdrop_list.rs +73 -3
  82. package/src/rust/components/containers/sidebar.rs +99 -22
  83. package/src/rust/components/containers/split_panel.rs +181 -62
  84. package/src/rust/components/context_menu.rs +130 -0
  85. package/src/rust/components/editable_header.rs +2 -0
  86. package/src/rust/components/form/debug.rs +3 -0
  87. package/src/rust/components/global_filter_bar.rs +76 -0
  88. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  89. package/src/rust/components/main_panel/msg.rs +62 -0
  90. package/src/rust/components/main_panel/presize.rs +220 -0
  91. package/src/rust/components/main_panel/reconcile.rs +224 -0
  92. package/src/rust/components/main_panel/render.rs +385 -0
  93. package/src/rust/components/main_panel/update.rs +184 -0
  94. package/src/rust/components/main_panel.rs +316 -71
  95. package/src/rust/components/mod.rs +5 -0
  96. package/src/rust/components/panel_menu.rs +493 -0
  97. package/src/rust/components/panel_tab.rs +510 -0
  98. package/src/rust/components/plugin_tab.rs +1 -1
  99. package/src/rust/components/portal.rs +7 -0
  100. package/src/rust/components/settings_panel.rs +13 -5
  101. package/src/rust/components/status_bar.rs +110 -178
  102. package/src/rust/components/status_indicator.rs +6 -9
  103. package/src/rust/components/style/mod.rs +5 -0
  104. package/src/rust/components/style/style_provider.rs +2 -0
  105. package/src/rust/components/viewer/filters.rs +133 -0
  106. package/src/rust/components/viewer/msg.rs +182 -0
  107. package/src/rust/components/viewer/panels.rs +326 -0
  108. package/src/rust/components/viewer/render.rs +255 -0
  109. package/src/rust/components/viewer/settings.rs +434 -0
  110. package/src/rust/components/viewer/snapshots.rs +134 -0
  111. package/src/rust/components/viewer/wiring.rs +505 -0
  112. package/src/rust/components/viewer.rs +245 -756
  113. package/src/rust/components/window_editor.rs +1013 -0
  114. package/src/rust/config/export_method.rs +1 -1
  115. package/src/rust/config/mod.rs +4 -0
  116. package/src/rust/config/options.rs +71 -0
  117. package/src/rust/config/viewer_config.rs +24 -2
  118. package/src/rust/config/workspace_config.rs +92 -0
  119. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  120. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  121. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  122. package/src/rust/custom_elements/viewer.rs +1015 -286
  123. package/src/rust/custom_events.rs +212 -61
  124. package/src/rust/js/mod.rs +2 -0
  125. package/src/rust/js/plugin.rs +297 -10
  126. package/src/rust/js/regular_layout.rs +258 -0
  127. package/src/rust/lib.rs +24 -2
  128. package/src/rust/presentation/column_locator.rs +16 -1
  129. package/src/rust/presentation.rs +66 -33
  130. package/src/rust/queries/columns_iter_set.rs +7 -1
  131. package/src/rust/queries/get_viewer_config.rs +29 -11
  132. package/src/rust/queries/plugin_column_styles.rs +2 -2
  133. package/src/rust/renderer/activate.rs +41 -14
  134. package/src/rust/renderer/dispatch.rs +463 -0
  135. package/src/rust/renderer/plugin_config.rs +495 -0
  136. package/src/rust/renderer/registry.rs +18 -6
  137. package/src/rust/renderer/render_timer.rs +29 -10
  138. package/src/rust/renderer.rs +384 -838
  139. package/src/rust/session/column_defaults_update.rs +26 -8
  140. package/src/rust/session/drag_drop_update.rs +18 -0
  141. package/src/rust/session/metadata.rs +66 -9
  142. package/src/rust/session/replace_expression_update.rs +1 -0
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +755 -264
  145. package/src/rust/tasks/apply_global_filters.rs +89 -0
  146. package/src/rust/tasks/auto_pause.rs +171 -0
  147. package/src/rust/tasks/copy_export.rs +29 -19
  148. package/src/rust/tasks/create_panel.rs +243 -0
  149. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  150. package/src/rust/tasks/edit_expression.rs +4 -4
  151. package/src/rust/tasks/edit_window.rs +155 -0
  152. package/src/rust/tasks/eject.rs +64 -23
  153. package/src/rust/tasks/mod.rs +30 -4
  154. package/src/rust/tasks/pipeline.rs +411 -0
  155. package/src/rust/tasks/presize_panels.rs +324 -0
  156. package/src/rust/tasks/reset_all.rs +22 -12
  157. package/src/rust/tasks/resize_observer.rs +124 -87
  158. package/src/rust/tasks/restore_and_render.rs +80 -86
  159. package/src/rust/tasks/restore_panel.rs +145 -0
  160. package/src/rust/tasks/send_column_config.rs +5 -2
  161. package/src/rust/tasks/send_plugin_config.rs +5 -2
  162. package/src/rust/tasks/set_edit_mode.rs +59 -0
  163. package/src/rust/tasks/sync_update_panels.rs +152 -0
  164. package/src/rust/tasks/table_lifecycle.rs +158 -0
  165. package/src/rust/tasks/update_theme.rs +56 -12
  166. package/src/rust/utils/browser/dragdrop.rs +12 -0
  167. package/src/rust/utils/browser/tests/debounce.rs +49 -0
  168. package/src/rust/utils/completion.rs +56 -0
  169. package/src/rust/utils/custom_element.rs +5 -0
  170. package/src/rust/utils/debounce.rs +189 -17
  171. package/src/rust/utils/mod.rs +6 -0
  172. package/src/rust/utils/pending_effects.rs +62 -0
  173. package/src/rust/utils/spawn.rs +33 -0
  174. package/src/rust/utils/weak_scope.rs +0 -10
  175. package/src/rust/workspace.rs +846 -0
  176. package/src/svg/active-indicator.svg +3 -0
  177. package/src/svg/pin-icon.svg +5 -0
  178. package/src/themes/botanical.css +5 -2
  179. package/src/themes/defaults.css +54 -2
  180. package/src/themes/dracula.css +6 -3
  181. package/src/themes/gruvbox-dark.css +5 -2
  182. package/src/themes/gruvbox.css +5 -2
  183. package/src/themes/icons.css +5 -2
  184. package/src/themes/intl/de.css +7 -0
  185. package/src/themes/intl/es.css +7 -0
  186. package/src/themes/intl/fr.css +7 -0
  187. package/src/themes/intl/ja.css +7 -0
  188. package/src/themes/intl/pt.css +7 -0
  189. package/src/themes/intl/zh.css +7 -0
  190. package/src/themes/intl.css +10 -1
  191. package/src/themes/monokai.css +5 -2
  192. package/src/themes/phosphor.css +5 -2
  193. package/src/themes/pro-dark.css +5 -2
  194. package/src/themes/pro.css +2 -1
  195. package/src/themes/solarized-dark.css +5 -2
  196. package/src/themes/solarized.css +5 -2
  197. package/src/themes/vaporwave.css +5 -2
  198. package/src/ts/bootstrap.ts +8 -7
  199. package/src/ts/extensions.ts +30 -2
  200. package/src/ts/perspective-viewer.ts +10 -0
  201. package/src/ts/plugin.ts +72 -5
  202. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  203. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  204. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  205. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  206. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  207. package/src/ts/ts-rs/Layout.ts +9 -0
  208. package/src/ts/ts-rs/Orientation.ts +6 -0
  209. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  210. package/src/ts/ts-rs/PanelViewerConfig.ts +16 -0
  211. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  212. package/src/ts/ts-rs/TabLayout.ts +11 -0
  213. package/src/ts/ts-rs/ViewerConfig.ts +7 -2
  214. package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
  215. package/src/ts/ts-rs/WindowAggregate.ts +3 -0
  216. package/src/ts/ts-rs/WindowSort.ts +9 -0
  217. package/src/ts/ts-rs/WindowSortDir.ts +8 -0
  218. package/src/ts/ts-rs/WindowSpec.ts +46 -0
  219. package/src/ts/ts-rs/Windows.ts +10 -0
  220. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  221. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  222. package/src/rust/tasks/intersection_observer.rs +0 -98
  223. package/src/rust/tasks/update_and_render.rs +0 -110
  224. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
  225. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
  226. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
  227. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
@@ -0,0 +1,89 @@
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
+ use perspective_client::config::ViewConfigUpdate;
14
+
15
+ use super::apply_and_render;
16
+ use crate::session::Session;
17
+ use crate::utils::spawn_owned;
18
+ use crate::workspace::{PanelId, Workspace};
19
+
20
+ /// Stamp `session`'s transient global-filter overlay from the
21
+ /// [`Workspace`]'s current set — EMPTY for master (filter-source) panels,
22
+ /// the full set for details. Master immunity is enforced here and only
23
+ /// here. Returns whether the overlay changed.
24
+ ///
25
+ /// Synchronous, so panel-creation paths can stamp BEFORE the panel's first
26
+ /// locked bind — the initial render then picks the overlay up via its
27
+ /// `ConfigSnapshot.effective`, with no second render and no unfiltered
28
+ /// first paint.
29
+ pub fn stamp_global_overlay(workspace: &Workspace, id: &PanelId, session: &Session) -> bool {
30
+ let filters = if workspace.is_master(id) {
31
+ Vec::new()
32
+ } else {
33
+ workspace.global_filters()
34
+ };
35
+
36
+ session.set_global_filter(filters)
37
+ }
38
+
39
+ /// Re-stamp every panel's overlay and re-render those whose overlay CHANGED
40
+ /// (details are usually non-active, so they need an explicit re-render
41
+ /// rather than relying on a subscription; unchanged panels aren't touched
42
+ /// at all).
43
+ pub fn apply_global_filters(workspace: &Workspace) {
44
+ for pid in workspace.panel_ids() {
45
+ if let Some(panel) = workspace.panel(&pid)
46
+ && stamp_global_overlay(workspace, &pid, &panel.session)
47
+ {
48
+ let effect = workspace.effects().guard();
49
+ let session = panel.session.clone();
50
+ let renderer = panel.renderer.clone();
51
+ spawn_owned("apply-global-filters", async move {
52
+ let _effect = effect;
53
+ apply_and_render(&session, &renderer, ViewConfigUpdate::default())?.await?;
54
+ Ok(())
55
+ });
56
+ }
57
+ }
58
+ }
59
+
60
+ /// Clear the ORIGINATING master panels' visible selection state (row
61
+ /// highlights, pinned tooltips) after their contributed clauses are removed
62
+ /// from the `GlobalFilterBar` (chip × / "Clear") — a selection visual must
63
+ /// not outlive the filter it produced. The plugin's OPTIONAL `deselect()` is
64
+ /// SILENT (no selection events), so no `MasterSelect` echo can re-mutate the
65
+ /// filter set; plugins without one (e.g. `Debug`) no-op. Each call runs
66
+ /// under its panel's draw lock — implementations may redraw (see the
67
+ /// call-discipline contract on `js/plugin.rs`). A closed panel's id is
68
+ /// skipped.
69
+ pub fn clear_master_selections(workspace: &Workspace, origins: Vec<PanelId>) {
70
+ for id in origins {
71
+ if let Some(panel) = workspace.panel(&id) {
72
+ let renderer = panel.renderer.clone();
73
+ spawn_owned("clear-master-selection", async move {
74
+ let r = renderer.clone();
75
+ renderer
76
+ .render_task(|_guard| async move {
77
+ // Pure query: a never-drawn panel has no plugin and
78
+ // no selection to clear.
79
+ if let Some(plugin) = r.active_plugin() {
80
+ plugin.deselect().await?;
81
+ }
82
+
83
+ Ok(())
84
+ })
85
+ .await
86
+ });
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,171 @@
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
+ use std::cell::Cell;
14
+ use std::rc::Rc;
15
+
16
+ use perspective_js::utils::global;
17
+ use wasm_bindgen::JsCast;
18
+ use wasm_bindgen::prelude::*;
19
+ use web_sys::*;
20
+
21
+ use crate::config::ViewerConfigUpdate;
22
+ use crate::js::*;
23
+ use crate::presentation::Presentation;
24
+ use crate::renderer::*;
25
+ use crate::session::Session;
26
+ use crate::utils::*;
27
+ use crate::workspace::Workspace;
28
+ use crate::*;
29
+
30
+ /// The `auto-pause` driver: pauses every panel's [`Session`] (deleting its
31
+ /// `ViewSubscription`, which stops `table_updated` at the source) whenever
32
+ /// the host element cannot be seen, and resumes with a re-render when it
33
+ /// can again. "Cannot be seen" is the conjunction of TWO independent
34
+ /// signals, either of which alone would miss real cases:
35
+ ///
36
+ /// - An `IntersectionObserver` on the host element (scrolled out of the
37
+ /// viewport, `display: none`, disconnected). This CANNOT observe
38
+ /// browser-tab backgrounding — intersection entries are only generated
39
+ /// during the document's rendering steps, which are suspended while hidden,
40
+ /// and the element's viewport intersection doesn't change on a tab switch
41
+ /// anyway.
42
+ /// - A `document` `visibilitychange` listener (tab backgrounded, window
43
+ /// minimized/occluded). Without it, a fast-updating table kept dispatching
44
+ /// update-redraws whose draws hang on the suspended rAF — the
45
+ /// background-tab reactivation stall.
46
+ pub struct AutoPauseHandle {
47
+ elem: HtmlElement,
48
+ observer: IntersectionObserver,
49
+ _callback: Closure<dyn FnMut(js_sys::Array)>,
50
+ _visibility: Closure<dyn Fn(JsValue)>,
51
+ }
52
+
53
+ impl AutoPauseHandle {
54
+ pub fn new(elem: &HtmlElement, presentation: &Presentation, workspace: &Workspace) -> Self {
55
+ let state = Rc::new(AutoPauseState {
56
+ intersecting: Cell::new(true),
57
+ doc_visible: Cell::new(!global::document().hidden()),
58
+ presentation: presentation.clone(),
59
+ workspace: workspace.clone(),
60
+ });
61
+
62
+ let _callback = Closure::new({
63
+ clone!(state);
64
+ move |xs: js_sys::Array| {
65
+ let intersect = xs
66
+ .pop()
67
+ .unchecked_into::<IntersectionObserverEntry>()
68
+ .is_intersecting();
69
+
70
+ state.intersecting.set(intersect);
71
+ state.apply();
72
+ }
73
+ });
74
+
75
+ let func = _callback.as_ref().unchecked_ref::<js_sys::Function>();
76
+ let observer = IntersectionObserver::new(func);
77
+ observer.observe(elem);
78
+
79
+ let _visibility = Closure::new({
80
+ clone!(state);
81
+ move |_: JsValue| {
82
+ state.doc_visible.set(!global::document().hidden());
83
+ state.apply();
84
+ }
85
+ });
86
+
87
+ global::document()
88
+ .add_event_listener_with_callback(
89
+ "visibilitychange",
90
+ _visibility.as_ref().unchecked_ref(),
91
+ )
92
+ .unwrap();
93
+
94
+ Self {
95
+ elem: elem.clone(),
96
+ observer,
97
+ _callback,
98
+ _visibility,
99
+ }
100
+ }
101
+ }
102
+
103
+ impl Drop for AutoPauseHandle {
104
+ fn drop(&mut self) {
105
+ self.observer.unobserve(&self.elem);
106
+ let _ = global::document().remove_event_listener_with_callback(
107
+ "visibilitychange",
108
+ self._visibility.as_ref().unchecked_ref(),
109
+ );
110
+ }
111
+ }
112
+
113
+ struct AutoPauseState {
114
+ intersecting: Cell<bool>,
115
+ doc_visible: Cell<bool>,
116
+ presentation: Presentation,
117
+ workspace: Workspace,
118
+ }
119
+
120
+ impl AutoPauseState {
121
+ fn apply(self: &Rc<Self>) {
122
+ let visible = self.intersecting.get() && self.doc_visible.get();
123
+ clone!(self.workspace, self.presentation);
124
+ ApiFuture::spawn(async move {
125
+ let panels = workspace.panels();
126
+ futures::future::join_all(panels.iter().map(|panel| {
127
+ let presentation = presentation.clone();
128
+ async move {
129
+ set_panel_paused(&panel.session, &panel.renderer, &presentation, visible).await
130
+ }
131
+ }))
132
+ .await
133
+ .into_iter()
134
+ .collect::<ApiResult<Vec<_>>>()?;
135
+
136
+ Ok(())
137
+ });
138
+ }
139
+ }
140
+
141
+ /// Apply one panel's pause/resume transition. Shared by the
142
+ /// [`AutoPauseState`] fan-out and `setAutoPause(false)`'s force-resume.
143
+ pub(crate) async fn set_panel_paused(
144
+ session: &Session,
145
+ renderer: &Renderer,
146
+ presentation: &Presentation,
147
+ visible: bool,
148
+ ) -> ApiResult<()> {
149
+ if visible {
150
+ if session.set_pause(false) {
151
+ let result = super::restore_and_render(
152
+ session,
153
+ renderer,
154
+ presentation,
155
+ super::RunOrigin::Internal,
156
+ ViewerConfigUpdate::default(),
157
+ async move { Ok(()) },
158
+ )
159
+ .await;
160
+
161
+ if let Err(e) = result.ignore_view_delete() {
162
+ session.set_run_error(e.clone()).await?;
163
+ return Err(e);
164
+ }
165
+ }
166
+ } else {
167
+ session.set_pause(true);
168
+ };
169
+
170
+ Ok(())
171
+ }
@@ -66,34 +66,44 @@ pub async fn html_as_jsvalue(
66
66
  }
67
67
 
68
68
  /// Render the current view as a `.png` `Blob` via the active plugin's
69
- /// `render` method (typically only available for chart plugins).
69
+ /// `render` method (typically only available for chart plugins). Chart
70
+ /// plugins implement `render` as a full internal `draw` + snapshot, so the
71
+ /// call must hold the renderer's draw lock like any other draw (see
72
+ /// [`crate::js::plugin`]) or it would interleave with a concurrent
73
+ /// `table_updated` redraw.
70
74
  pub async fn png_as_jsvalue(session: &Session, renderer: &Renderer) -> ApiResult<web_sys::Blob> {
71
- let plugin = renderer.get_active_plugin()?;
72
- let view: perspective_client::View = session
73
- .get_view()
74
- .ok_or(ApiError::from(ApiErrorType::NoTableError))?;
75
-
76
- let png = plugin.render(view.into(), None).await?;
77
- Ok(png)
75
+ renderer
76
+ .clone()
77
+ .render_task(|_guard| async move {
78
+ let plugin = renderer.ensure_plugin_selected()?;
79
+ let view: perspective_client::View = session
80
+ .get_view()
81
+ .ok_or(ApiError::from(ApiErrorType::NoTableError))?;
82
+
83
+ plugin.render(view.into(), None).await
84
+ })
85
+ .await
78
86
  }
79
87
 
80
88
  /// Render the current view as a `.txt` `Blob` via the active plugin's
81
- /// `render` method (typically used for the datagrid).
89
+ /// `render` method (typically used for the datagrid). Locked for the same
90
+ /// reason as [`png_as_jsvalue`].
82
91
  pub async fn txt_as_jsvalue(
83
92
  session: &Session,
84
93
  renderer: &Renderer,
85
94
  viewport: Option<ViewWindow>,
86
95
  ) -> ApiResult<web_sys::Blob> {
87
- let plugin = renderer.get_active_plugin()?;
88
- let view: perspective_client::View = session
89
- .get_view()
90
- .ok_or(ApiError::from(ApiErrorType::NoTableError))?;
91
-
92
- let txt = plugin
93
- .render(view.into(), viewport.map(|x| x.into()))
94
- .await?;
95
-
96
- Ok(txt)
96
+ renderer
97
+ .clone()
98
+ .render_task(|_guard| async move {
99
+ let plugin = renderer.ensure_plugin_selected()?;
100
+ let view: perspective_client::View = session
101
+ .get_view()
102
+ .ok_or(ApiError::from(ApiErrorType::NoTableError))?;
103
+
104
+ plugin.render(view.into(), viewport.map(|x| x.into())).await
105
+ })
106
+ .await
97
107
  }
98
108
 
99
109
  /// Generate a result `Blob` for all types of [`ExportMethod`].
@@ -0,0 +1,243 @@
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
+ use web_sys::HtmlElement;
14
+ use yew::Callback;
15
+
16
+ use crate::config::*;
17
+ use crate::custom_events::wire_panel_events;
18
+ use crate::presentation::*;
19
+ use crate::renderer::*;
20
+ use crate::session::{ResetOptions, Session};
21
+ use crate::tasks::*;
22
+ use crate::utils::*;
23
+ use crate::workspace::{Panel, PanelId, Workspace};
24
+ use crate::*;
25
+
26
+ /// Build the full set of subscriptions a [`Panel`] owns for its lifetime: its
27
+ /// redraw subscription plus its custom-event fanout ([`wire_panel_events`]).
28
+ /// Shared by the seed (element constructor) and every [`create_panel_model`]
29
+ /// panel, so all panels wire identically and every panel — not just the seed —
30
+ /// dispatches its own `perspective-*` events (see C6).
31
+ pub(crate) fn wire_panel_subs(
32
+ elem: &HtmlElement,
33
+ presentation: &Presentation,
34
+ session: &Session,
35
+ renderer: &Renderer,
36
+ ) -> Vec<Subscription> {
37
+ let mut subs = vec![wire_panel_render_sub(session, renderer)];
38
+ subs.extend(wire_panel_events(elem, session, renderer, presentation));
39
+ subs
40
+ }
41
+
42
+ /// Wire a panel's data-refresh subscription: when its [`Session`]'s table emits
43
+ /// an update, redraw its [`Renderer`]. The returned [`Subscription`] must be
44
+ /// owned for the panel's lifetime (see [`Panel`]).
45
+ ///
46
+ /// Hidden panels (an unslotted tab-stack panel, a `display: none` host)
47
+ /// don't redraw — a full charts dispatch per update is a fetch + render
48
+ /// nobody can see. The dropped update is recorded on the [`Renderer`]
49
+ /// instead, and the activation nudge (`tasks::pipeline::activation_render`)
50
+ /// consumes the marker to catch the panel up with ONE `plugin.update`.
51
+ fn wire_panel_render_sub(session: &Session, renderer: &Renderer) -> Subscription {
52
+ session.table_updated.add_listener({
53
+ clone!(renderer, session);
54
+ move |_| {
55
+ if renderer.is_plugin_hidden() {
56
+ renderer.set_data_stale();
57
+ return;
58
+ }
59
+
60
+ clone!(renderer, session);
61
+ ApiFuture::spawn(async move {
62
+ renderer
63
+ .update_lazy(async move { Ok(session.get_view()) })
64
+ .await
65
+ .ignore_view_delete()
66
+ .map(|_| ())
67
+ })
68
+ }
69
+ })
70
+ }
71
+
72
+ /// Create a new independent panel (own `Session` + `Renderer` + id) from a
73
+ /// `ViewerConfigUpdate`, mount and draw it, and return its id. Shared by
74
+ /// `addPanel`, whole-element `restore`, and `restorePanel`'s create-if-missing
75
+ /// path. `id` is the panel's id — provided when restoring into a specific named
76
+ /// slot, or `None` to generate a fresh one. `settings`/`theme` are stripped
77
+ /// (element-level, not per-panel) and `client` — or the element's default
78
+ /// client when `None` — is bound so the config's `table` resolves against it.
79
+ pub(crate) async fn create_panel(
80
+ elem: &HtmlElement,
81
+ presentation: &Presentation,
82
+ workspace: &Workspace,
83
+ notify: &Callback<()>,
84
+ id: Option<PanelId>,
85
+ update: ViewerConfigUpdate,
86
+ client: Option<perspective_client::Client>,
87
+ ) -> ApiResult<PanelId> {
88
+ let (id, session, renderer, update) = create_panel_model(
89
+ elem,
90
+ presentation,
91
+ workspace,
92
+ id,
93
+ update,
94
+ client,
95
+ Placement::Placed,
96
+ );
97
+
98
+ stamp_global_overlay(workspace, &id, &session);
99
+ // STAGE the panel (model-first): `MainPanel` renders its plugin slot
100
+ // into a hidden pre-sized wrapper — not a layout cell — so the restore
101
+ // below completes its first draw invisibly at (near-)final size. The
102
+ // promote below then re-renders, and `MainPanel::reconcile` inserts an
103
+ // ALREADY-DRAWN panel: the one layout transition reveals content,
104
+ // never a blank frame.
105
+ workspace.stage_panel(&id);
106
+ notify.emit(());
107
+
108
+ // The staging deadline: a slow restore promotes EARLY (placed, still
109
+ // loading — the pre-staging progressive reveal) rather than leaving
110
+ // the layout without feedback. Races the completion promote below on
111
+ // the staged flag; only the winner re-renders.
112
+ {
113
+ clone!(workspace, id, notify);
114
+ ApiFuture::spawn(async move {
115
+ set_timeout(STAGING_DEADLINE_MS).await?;
116
+ if workspace.clear_staged(&id) {
117
+ notify.emit(());
118
+ }
119
+
120
+ Ok(())
121
+ });
122
+ }
123
+
124
+ // A fresh panel is never the active one, so it needs no `root` for the
125
+ // (active-only) settings-sidebar sequencing.
126
+ let result = restore_panel(
127
+ &session,
128
+ &renderer,
129
+ presentation,
130
+ workspace,
131
+ None,
132
+ RestoreMode::Fresh,
133
+ update,
134
+ )
135
+ .await;
136
+
137
+ // Promote on completion AND error — a failed restore's error state
138
+ // must become visible too.
139
+ if workspace.clear_staged(&id) {
140
+ notify.emit(());
141
+ }
142
+
143
+ result?;
144
+ Ok(id)
145
+ }
146
+
147
+ /// Where [`create_panel_model`] registers the new panel model.
148
+ pub(crate) enum Placement {
149
+ /// Into the placed panel set ([`Workspace::insert_panel`]) — a live,
150
+ /// visible panel from the start.
151
+ Placed,
152
+
153
+ /// Into the reservation slot ([`Workspace::reserve_panel`]) — a pending
154
+ /// `load()`'s first panel, held out of the placed set until it is
155
+ /// claimed ([`place_reserved`]) or discarded.
156
+ Reserved,
157
+ }
158
+
159
+ /// The synchronous *model* half of [`create_panel`]: build and register a new
160
+ /// panel's engine handles (own `Session` + `Renderer` + id) in the
161
+ /// [`Workspace`] (per `placement`), apply-and-strip the element-level config
162
+ /// fields (`settings`/`theme`), and bind `client` (falling back to the default
163
+ /// client). `id` is the panel's id, or `None` to generate a fresh one.
164
+ pub(crate) fn create_panel_model(
165
+ elem: &HtmlElement,
166
+ presentation: &Presentation,
167
+ workspace: &Workspace,
168
+ id: Option<PanelId>,
169
+ mut update: ViewerConfigUpdate,
170
+ client: Option<perspective_client::Client>,
171
+ placement: Placement,
172
+ ) -> (PanelId, Session, Renderer, ViewerConfigUpdate) {
173
+ // Authored-theme boot: the FIRST panel of an empty element with no explicit
174
+ // theme adopts the element's `theme` attribute (a one-time initial
175
+ // selection; the attribute is otherwise an active-panel mirror). Captured
176
+ // before `insert_panel` makes the workspace non-empty.
177
+ let boot_theme = (workspace.is_empty() && matches!(update.theme, OptionalUpdate::Missing))
178
+ .then(|| elem.get_attribute("theme"))
179
+ .flatten();
180
+
181
+ let session = Session::new();
182
+ let renderer = Renderer::new(elem);
183
+ let id = id.unwrap_or_else(|| workspace.generate_id());
184
+ renderer.set_slot_name(id.as_str());
185
+ renderer.set_default_theme(presentation.default_theme_name_sync());
186
+ let subs = wire_panel_subs(elem, presentation, &session, &renderer);
187
+ let panel = Panel::new(id.clone(), session.clone(), renderer.clone(), subs);
188
+ match placement {
189
+ Placement::Placed => workspace.insert_panel(panel),
190
+ Placement::Reserved => workspace.reserve_panel(panel),
191
+ }
192
+
193
+ update.settings = OptionalUpdate::Missing;
194
+ if let OptionalUpdate::Update(theme) = &update.theme {
195
+ renderer.set_theme(Some(theme.clone()));
196
+ } else if let Some(theme) = boot_theme {
197
+ renderer.set_theme(Some(theme));
198
+ }
199
+
200
+ update.theme = OptionalUpdate::Missing;
201
+ if let Some(client) = client.or_else(|| workspace.default_client()) {
202
+ workspace.register_client(client.clone());
203
+ session.set_client(client);
204
+ }
205
+
206
+ if let Some((idx, _)) = renderer.resolve_plugin_update(&update.plugin) {
207
+ let _ = renderer.commit_plugin(Some(idx));
208
+ let _ = renderer.mount_active_plugin();
209
+ }
210
+
211
+ (id, session, renderer, update)
212
+ }
213
+
214
+ pub(crate) fn place_reserved(workspace: &Workspace, notify: &Callback<()>) -> Option<Panel> {
215
+ let panel = workspace.claim_reserved()?;
216
+ stamp_global_overlay(workspace, &panel.id, &panel.session);
217
+ notify.emit(());
218
+ Some(panel)
219
+ }
220
+
221
+ /// Tear down a [`Panel`] already removed from the [`Workspace`]: dispose its
222
+ /// renderer (slot-scoped plugin + light-DOM cleanup) and eject its table.
223
+ /// Shared by the root's `ClosePanel` handler and whole-element `restore`'s
224
+ /// batch replacement of the pre-existing panel set.
225
+ pub(crate) fn eject_panel(panel: Panel) -> ApiFuture<()> {
226
+ let was_errored = panel.session.is_errored();
227
+ let dispose_task = panel.renderer.dispose();
228
+ let reset_task = panel.session.reset(ResetOptions {
229
+ config: true,
230
+ expressions: true,
231
+ table: Some(session::TableIntermediateState::Ejected),
232
+ ..ResetOptions::default()
233
+ });
234
+
235
+ ApiFuture::new(async move {
236
+ dispose_task.await?;
237
+ match reset_task.await.ignore_view_delete() {
238
+ Err(_) if was_errored => Ok(()),
239
+ Err(e) => Err(e),
240
+ Ok(_) => Ok(()),
241
+ }
242
+ })
243
+ }
@@ -27,7 +27,9 @@ pub fn dismiss_render_warning_callback(session: &Session, renderer: &Renderer) -
27
27
  clone!(session, renderer);
28
28
  ApiFuture::spawn(async move {
29
29
  renderer.disable_active_plugin_render_warning();
30
- renderer.update(session.get_view()).await
30
+ renderer
31
+ .update_lazy(async move { Ok(session.get_view()) })
32
+ .await
31
33
  });
32
34
  })
33
35
  }
@@ -17,7 +17,7 @@
17
17
  use perspective_client::clone;
18
18
  use perspective_client::config::{Expression, ViewConfigUpdate};
19
19
 
20
- use super::update_and_render;
20
+ use super::apply_and_render;
21
21
  use crate::presentation::{ColumnLocator, ColumnSettingsTab, OpenColumnSettings, Presentation};
22
22
  use crate::renderer::Renderer;
23
23
  use crate::session::Session;
@@ -39,7 +39,7 @@ pub fn update_expr(
39
39
  .to_props()
40
40
  .create_replace_expression_update(&old_name, &new_expr);
41
41
 
42
- update_and_render(&session, &renderer, update)?.await?;
42
+ apply_and_render(&session, &renderer, update)?.await?;
43
43
  presentation.set_open_column_settings(Some(OpenColumnSettings {
44
44
  locator: Some(ColumnLocator::Expression(new_expr.name.to_string())),
45
45
  tab: Some(ColumnSettingsTab::Attributes),
@@ -62,7 +62,7 @@ pub fn save_expr(
62
62
  let task = {
63
63
  let mut serde_exprs = session.get_view_config().expressions.clone();
64
64
  serde_exprs.insert(&expr);
65
- update_and_render(session, renderer, ViewConfigUpdate {
65
+ apply_and_render(session, renderer, ViewConfigUpdate {
66
66
  expressions: Some(serde_exprs),
67
67
  ..Default::default()
68
68
  })
@@ -90,7 +90,7 @@ pub fn delete_expr(session: &Session, renderer: &Renderer, expr_name: &str) -> A
90
90
  ..ViewConfigUpdate::default()
91
91
  };
92
92
 
93
- let task = update_and_render(session, renderer, config)?;
93
+ let task = apply_and_render(session, renderer, config)?;
94
94
  ApiFuture::spawn(task);
95
95
  Ok(())
96
96
  }