@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,158 @@
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::RefCell;
14
+ use std::rc::Rc;
15
+
16
+ use perspective_client::utils::PerspectiveResultExt;
17
+ use perspective_client::{Client, clone};
18
+ use perspective_js::utils::{ApiFuture, ApiResult, LocalPollLoop};
19
+ use wasm_bindgen::JsValue;
20
+
21
+ use super::pipeline::RunOrigin;
22
+ use super::restore_and_render::restore_and_render;
23
+ use crate::config::ViewerConfigUpdate;
24
+ use crate::presentation::Presentation;
25
+ use crate::utils::{AddListener, Subscription};
26
+ use crate::workspace::{Panel, Workspace};
27
+
28
+ /// The per-client hosted-tables-update subscriptions this element holds, for
29
+ /// removal at `delete()`.
30
+ pub(crate) type HostedTableSubs = Rc<RefCell<Vec<(Client, u32)>>>;
31
+
32
+ /// Wire reactive table binding for an element: subscribe every registered
33
+ /// [`Client`]'s hosted-tables updates (current and future registrations) to
34
+ /// [`sweep_table_bindings`]. Returns the registration listener (owned for the
35
+ /// element's lifetime) and the accumulated client subscriptions (removed at
36
+ /// element `delete()`).
37
+ pub(crate) fn wire_table_lifecycle(
38
+ workspace: &Workspace,
39
+ presentation: &Presentation,
40
+ ) -> (Subscription, HostedTableSubs) {
41
+ let subs: HostedTableSubs = Rc::default();
42
+ let sub = workspace.client_registered().add_listener({
43
+ clone!(workspace, presentation, subs);
44
+ move |client: Client| {
45
+ clone!(workspace, presentation, subs);
46
+ ApiFuture::spawn(async move {
47
+ let poll_loop = LocalPollLoop::new({
48
+ clone!(workspace, presentation);
49
+ move |()| {
50
+ clone!(workspace, presentation);
51
+ ApiFuture::spawn(async move {
52
+ sweep_table_bindings(&workspace, &presentation).await
53
+ });
54
+
55
+ Ok(JsValue::UNDEFINED)
56
+ }
57
+ });
58
+
59
+ let id = client
60
+ .on_hosted_tables_update(move || {
61
+ let poll_loop = poll_loop.clone();
62
+ async move {
63
+ poll_loop.poll(()).await;
64
+ }
65
+ })
66
+ .await?;
67
+
68
+ subs.borrow_mut().push((client, id));
69
+
70
+ // A table created between this client's registration and the
71
+ // subscription above fired no callback — sweep NOW, after
72
+ // subscribing, so no creation can be missed.
73
+ sweep_table_bindings(&workspace, &presentation).await
74
+ })
75
+ }
76
+ });
77
+
78
+ (sub, subs)
79
+ }
80
+
81
+ /// One reconciliation pass over every panel's table binding vs. the loaded
82
+ /// clients' hosted sets: releases first (a delete and a re-create in the same
83
+ /// update settle as suspend-then-rebind), then completes pending binds.
84
+ /// Per-panel failures are logged, never aborting the sweep.
85
+ pub(crate) async fn sweep_table_bindings(
86
+ workspace: &Workspace,
87
+ presentation: &Presentation,
88
+ ) -> ApiResult<()> {
89
+ for panel in workspace.panels() {
90
+ if let Some(name) = panel.session.get_table().map(|t| t.get_name().to_owned())
91
+ && let Some(client) = panel.session.get_client()
92
+ && let Ok(hosted) = client.get_hosted_table_names().await
93
+ && !hosted.iter().any(|n| n == &name)
94
+ {
95
+ suspend_panel(&panel).await.unwrap_or_log();
96
+ }
97
+ }
98
+
99
+ for panel in workspace.panels() {
100
+ if panel.session.pending_table().is_some() {
101
+ bind_pending(&panel, workspace, presentation)
102
+ .await
103
+ .unwrap_or_log();
104
+ }
105
+ }
106
+
107
+ Ok(())
108
+ }
109
+
110
+ /// Suspend one panel's binding under its draw lock (see
111
+ /// [`Session::suspend_table`]) and blank its display. The suspend re-derives
112
+ /// the bound name inside the lock — a panel that rebound or ejected since the
113
+ /// sweep's observation suspends its CURRENT state or no-ops.
114
+ async fn suspend_panel(panel: &Panel) -> ApiResult<()> {
115
+ clone!(panel.session, panel.renderer);
116
+ renderer
117
+ .clone()
118
+ .render_task(|_guard| async move {
119
+ if let Some(reset) = session.suspend_table() {
120
+ reset.await?;
121
+ if let Some(plugin) = renderer.active_plugin() {
122
+ plugin.clear().await?;
123
+ }
124
+ }
125
+
126
+ Ok(())
127
+ })
128
+ .await
129
+ }
130
+
131
+ /// Complete a PENDING panel's bind through the shared restore pipeline: the
132
+ /// committed view config is untouched, the pending name re-derived inside the
133
+ /// locked run (capture-free), the client federated exactly as a `restore`
134
+ /// would. A name still un-hosted (the sweep raced a delete) simply re-pends.
135
+ async fn bind_pending(
136
+ panel: &Panel,
137
+ workspace: &Workspace,
138
+ presentation: &Presentation,
139
+ ) -> ApiResult<()> {
140
+ restore_and_render(
141
+ &panel.session,
142
+ &panel.renderer,
143
+ presentation,
144
+ RunOrigin::Internal,
145
+ ViewerConfigUpdate::default(),
146
+ {
147
+ clone!(panel.session, workspace);
148
+ async move {
149
+ let Some(name) = session.pending_table() else {
150
+ return Ok(());
151
+ };
152
+
153
+ super::restore_panel::bind_table_task(&session, &workspace, name).await
154
+ }
155
+ },
156
+ )
157
+ .await
158
+ }
@@ -10,31 +10,63 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- //! Theme reset / set task. Replaces the inlined StatusBar callback that
14
- //! reached into `Session::get_view()` for the post-update restyle.
13
+ //! Theme reset / set task.
15
14
 
15
+ use futures::future::join_all;
16
16
  use perspective_js::utils::*;
17
17
 
18
18
  use crate::presentation::Presentation;
19
19
  use crate::renderer::Renderer;
20
- use crate::session::Session;
20
+ use crate::workspace::Workspace;
21
21
 
22
- /// Apply a theme change and restyle the active view.
22
+ /// Re-seed every panel renderer's cached registry default theme from the
23
+ /// (awaited, so initialized) theme registry, returning the default name.
24
+ /// Every consumer of `Renderer::needs_restyle` after a registry-affecting
25
+ /// change must run this first — a cold cache compares against `None`.
26
+ pub(crate) async fn seed_default_themes(
27
+ presentation: &Presentation,
28
+ workspace: &Workspace,
29
+ ) -> Option<String> {
30
+ let default = presentation.get_default_theme_name().await;
31
+ for panel in workspace.panels() {
32
+ panel.renderer.set_default_theme(default.clone());
33
+ }
34
+
35
+ default
36
+ }
37
+
38
+ /// Apply a theme change and restyle the affected panel's view.
23
39
  ///
24
40
  /// `theme = None` resets to the first available theme; `theme = Some(name)`
25
- /// sets the named theme. In both cases the post-update path is the same:
26
- /// fetch the current view from the session and ask the renderer to
27
- /// re-style it. Components dispatch this task instead of reading
41
+ /// sets the named theme. The `theme` is recorded on the active `renderer`
42
+ /// (per-panel state) and mirrored onto the host `theme` attribute (driving
43
+ /// the shared chrome). Components dispatch this task instead of reading
28
44
  /// `Session::get_view()` themselves.
45
+ ///
46
+ /// Every panel's plugin carries its own stamped `theme` attribute (effective
47
+ /// theme = its own, else the registry default — see `Renderer::stamp_theme`),
48
+ /// and the theme rules provide a COMPLETE var set for `perspective-viewer
49
+ /// [theme="X"]` descendants (no inheritance from the host attribute), so a
50
+ /// pick changes ONLY the picked panel's effective theme — the host flip
51
+ /// restyles nothing else. Restyle (an expensive full `restyle_all`, which
52
+ /// also redraws) is therefore scoped to panels whose captured CSS is
53
+ /// actually STALE (`Renderer::needs_restyle` — effective theme vs. the one
54
+ /// stamped at the plugin's last capture): the picked panel when the value
55
+ /// is genuinely new, and never a panel that has yet to first-paint.
29
56
  pub fn update_theme(
30
- session: &Session,
31
57
  renderer: &Renderer,
32
58
  presentation: &Presentation,
59
+ workspace: &Workspace,
33
60
  theme: Option<String>,
34
61
  ) {
35
- let session = session.clone();
36
- let renderer = renderer.clone();
62
+ // Per-panel: record the theme on the (active) renderer so this panel keeps
63
+ // it independent of which panel is active. `set_theme_name` below mirrors
64
+ // the same value onto the host `theme` attribute (driving the chrome), and
65
+ // MainPanel inlines this renderer's theme on its frame only when it
66
+ renderer.set_theme_stamped(theme.clone());
67
+
37
68
  let presentation = presentation.clone();
69
+ let workspace = workspace.clone();
38
70
  ApiFuture::spawn(async move {
39
71
  match theme {
40
72
  Some(name) => {
@@ -45,7 +77,19 @@ pub fn update_theme(
45
77
  },
46
78
  }
47
79
 
48
- let view = session.get_view().into_apierror()?;
49
- renderer.restyle_all(&view).await
80
+ seed_default_themes(&presentation, &workspace).await;
81
+ let panels = workspace.panels();
82
+ join_all(panels.iter().map(|panel| async move {
83
+ if panel.renderer.needs_restyle() {
84
+ panel.renderer.restyle_all().await?;
85
+ }
86
+
87
+ ApiResult::<()>::Ok(())
88
+ }))
89
+ .await
90
+ .into_iter()
91
+ .collect::<ApiResult<Vec<_>>>()?;
92
+
93
+ Ok(())
50
94
  });
51
95
  }
@@ -17,6 +17,18 @@ pub enum DragTarget {
17
17
  SplitBy,
18
18
  Sort,
19
19
  Filter,
20
+ WindowSource,
21
+ WindowOrderBy,
22
+ WindowPartitionBy,
23
+ }
24
+
25
+ impl DragTarget {
26
+ pub fn is_staged(&self) -> bool {
27
+ matches!(
28
+ self,
29
+ Self::WindowSource | Self::WindowOrderBy | Self::WindowPartitionBy
30
+ )
31
+ }
20
32
  }
21
33
 
22
34
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -86,6 +86,55 @@ pub async fn test_lock_seq() {
86
86
  assert_eq!(cell.get(), 10);
87
87
  }
88
88
 
89
+ /// A dedicated [`DebounceSlot`]'s task must RUN even when the default slot
90
+ /// has a parked runner — coalescing is scoped per slot. Pre-slot (one
91
+ /// shared parked flag), `t4` would resolve via `t2`'s settle without its
92
+ /// closure ever running: the resize-eaten-by-parked-update bug.
93
+ #[wasm_bindgen_test]
94
+ pub async fn test_slot_isolated_from_default_coalescing() {
95
+ let debounce_mutex = DebounceMutex::default();
96
+ let slot = debounce_mutex.slot();
97
+ let defaults: Rc<Cell<u32>> = Rc::new(Cell::new(0));
98
+ let slots: Rc<Cell<u32>> = Rc::new(Cell::new(0));
99
+
100
+ let t1 = debounce_mutex.lock(async {
101
+ set_timeout(10).await.unwrap();
102
+ });
103
+
104
+ let t2 = {
105
+ let defaults = defaults.clone();
106
+ debounce_mutex.debounce(async move {
107
+ defaults.set(defaults.get() + 1);
108
+ Ok(())
109
+ })
110
+ };
111
+
112
+ let t3 = {
113
+ let defaults = defaults.clone();
114
+ debounce_mutex.debounce(async move {
115
+ defaults.set(defaults.get() + 100);
116
+ Ok(())
117
+ })
118
+ };
119
+
120
+ let t4 = {
121
+ let slots = slots.clone();
122
+ slot.debounce_with(|_| async move {
123
+ slots.set(slots.get() + 1);
124
+ Ok(())
125
+ })
126
+ };
127
+
128
+ let (_, r2, r3, r4) = futures::join!(t1, t2, t3, t4);
129
+ r2.unwrap();
130
+ r3.unwrap();
131
+ r4.unwrap();
132
+
133
+ // `t3` coalesced onto the parked `t2`; `t4` ran its own closure.
134
+ assert_eq!(defaults.get(), 1);
135
+ assert_eq!(slots.get(), 1);
136
+ }
137
+
89
138
  #[wasm_bindgen_test]
90
139
  pub async fn test_debounce_seq() {
91
140
  let debounce_mutex = DebounceMutex::default();
@@ -0,0 +1,56 @@
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::future::Future;
14
+
15
+ use futures::channel::oneshot;
16
+ use perspective_js::utils::*;
17
+
18
+ /// A completion handle for message-based public API methods (invariant I6 —
19
+ /// see `SESSION_CONFIG_COHERENCE_PLAN.md`): its ONLY resolution API is
20
+ /// [`Completion::resolve_after`], which takes a run future — so resolving a
21
+ /// public method's promise at message-handling time (before the renders it
22
+ /// caused have drawn) is unwritable, not merely discouraged. Dropping an
23
+ /// unresolved `Completion` rejects the caller as cancelled.
24
+ pub struct Completion(Option<oneshot::Sender<ApiResult<()>>>);
25
+
26
+ impl Completion {
27
+ #[allow(clippy::new_ret_no_self)]
28
+ pub fn new() -> (Self, oneshot::Receiver<ApiResult<()>>) {
29
+ let (sender, receiver) = oneshot::channel();
30
+ (Self(Some(sender)), receiver)
31
+ }
32
+
33
+ /// Resolve this completion with `run`'s result when it settles. The
34
+ /// caller relinquishes the handle — one run, one resolution.
35
+ pub fn resolve_after(mut self, run: impl Future<Output = ApiResult<()>> + 'static) {
36
+ let sender = self.0.take().unwrap();
37
+ ApiFuture::spawn(async move {
38
+ let _ = sender.send(run.await);
39
+ Ok(())
40
+ });
41
+ }
42
+ }
43
+
44
+ impl std::fmt::Debug for Completion {
45
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46
+ f.debug_struct("Completion").finish()
47
+ }
48
+ }
49
+
50
+ impl Drop for Completion {
51
+ fn drop(&mut self) {
52
+ if let Some(sender) = self.0.take() {
53
+ let _ = sender.send(Err(ApiError::new("Cancelled")));
54
+ }
55
+ }
56
+ }
@@ -16,6 +16,11 @@ use wasm_bindgen::prelude::*;
16
16
 
17
17
  #[wasm_bindgen(inline_js = r#"
18
18
  export function bootstrap(psp, name, clsname, statics) {
19
+ if (customElements.get(name)) {
20
+ console.warn(`Custom element "${name}" is already registered; skipping duplicate registration (Perspective was loaded more than once on this page).`);
21
+ return;
22
+ }
23
+
19
24
  const cls = psp[clsname];
20
25
  const proto = cls.prototype;
21
26
  class x extends HTMLElement {
@@ -17,44 +17,216 @@ use std::rc::Rc;
17
17
  use async_lock::Mutex;
18
18
  use perspective_js::utils::ApiResult;
19
19
 
20
+ use super::pubsub::PubSub;
21
+
22
+ /// Proof that the bearer is executing inside a [`DebounceMutex`]-locked task.
23
+ ///
24
+ /// Every plugin-dispatching function (`draw_view`, `activate_plugin`, the
25
+ /// restyle/resize/export wrappers) and the snapshot pipeline
26
+ /// (`bind_snapshot`) require `&RenderGuard`, so an unlocked plugin call or
27
+ /// out-of-pipeline render is a missing witness — a compile error, not a
28
+ /// review rule. Constructed ONLY by [`DebounceMutex::lock_with`] /
29
+ /// [`DebounceMutex::debounce_with`], after the lock is acquired; it is not
30
+ /// `Clone`, so it cannot be stashed for use outside the task that received
31
+ /// it.
32
+ pub struct RenderGuard {
33
+ _private: (),
34
+ }
35
+
36
+ /// The per-slot debounce bookkeeping: one running + one parked evaluation,
37
+ /// with coalesced callers awaiting the parked runner's settle. Kept
38
+ /// per-[`DebounceSlot`] so coalescing is only expressible WITHIN a kind —
39
+ /// a caller can never be absorbed by a parked runner of a different kind
40
+ /// whose work does not subsume it (the resize-eaten-by-parked-update bug).
20
41
  #[derive(Default)]
21
- struct DebounceMutexData {
42
+ struct SlotState {
22
43
  id: Cell<u64>,
23
- mutex: Mutex<u64>,
44
+ last: Cell<u64>,
45
+ parked: Cell<bool>,
46
+ settled_id: Cell<u64>,
47
+ on_settle: PubSub<()>,
48
+ }
49
+
50
+ #[derive(Default)]
51
+ struct DebounceMutexData {
52
+ held: Cell<bool>,
53
+ mutex: Mutex<()>,
54
+ default_slot: Rc<SlotState>,
55
+ }
56
+
57
+ /// Clears the `held` flag on drop, so cancellation of a locked task (its
58
+ /// future dropped mid-await) can't leave [`DebounceMutex::is_held`] stuck
59
+ /// `true`.
60
+ struct HeldFlag<'a>(&'a Cell<bool>);
61
+
62
+ impl<'a> HeldFlag<'a> {
63
+ fn set(cell: &'a Cell<bool>) -> Self {
64
+ cell.set(true);
65
+ Self(cell)
66
+ }
67
+ }
68
+
69
+ impl Drop for HeldFlag<'_> {
70
+ fn drop(&mut self) {
71
+ self.0.set(false);
72
+ }
73
+ }
74
+
75
+ /// RAII for the parked debounce runner: clears `parked` and broadcasts the
76
+ /// runner's settle on drop. Running through `Drop` (not a happy-path call)
77
+ /// makes the release unconditional — a runner cancelled mid-park or
78
+ /// mid-run still frees the parked slot and releases its coalesced waiters,
79
+ /// so they can never be stranded awaiting a settle that no task will emit.
80
+ struct SettleGuard<'a> {
81
+ state: &'a SlotState,
82
+ next: u64,
83
+ }
84
+
85
+ impl<'a> SettleGuard<'a> {
86
+ fn park(state: &'a SlotState, next: u64) -> Self {
87
+ state.parked.set(true);
88
+ Self { state, next }
89
+ }
90
+ }
91
+
92
+ impl Drop for SettleGuard<'_> {
93
+ fn drop(&mut self) {
94
+ self.state.parked.set(false);
95
+ if self.state.settled_id.get() < self.next {
96
+ self.state.settled_id.set(self.next);
97
+ }
98
+
99
+ self.state.on_settle.emit(());
100
+ }
24
101
  }
25
102
 
26
103
  /// An async `Mutex` type specialized for Perspective's rendering, which
27
- /// debounces calls in addition to providing exclusivity. Calling `debounce`
28
- /// with a _cancellable_ [`Future`] will resolve only after at least one
29
- /// _complete_ evaluation of a call awaiting the lock.
104
+ /// debounces calls in addition to providing exclusivity. Calling
105
+ /// [`Self::debounce`] resolves only after at least one complete evaluation
106
+ /// of a call that began no earlier than this one — either by running the
107
+ /// caller's own future, or by coalescing onto the single parked trailing
108
+ /// runner. At most TWO debounce evaluations are ever outstanding (one
109
+ /// running, one parked); every additional concurrent caller resolves
110
+ /// without queueing on the lock.
30
111
  #[derive(Clone, Default)]
31
112
  pub struct DebounceMutex(Rc<DebounceMutexData>);
32
113
 
33
114
  impl DebounceMutex {
115
+ /// `true` while a locked task is executing. Used by lock-acquiring
116
+ /// public API methods to emit a debug-build warning when they are about
117
+ /// to queue behind an in-flight run — legitimate for app callers, a
118
+ /// guaranteed deadlock when reached synchronously from a plugin's render
119
+ /// (the render-callable contract on `js::plugin` forbids it).
120
+ pub fn is_held(&self) -> bool {
121
+ self.0.held.get()
122
+ }
123
+
34
124
  /// Lock like a normal `Mutex`.
35
125
  pub async fn lock<T>(&self, f: impl Future<Output = T>) -> T {
36
- let mut last = self.0.mutex.lock().await;
37
- let next = self.0.id.get();
38
- let result = f.await;
39
- *last = next;
126
+ self.lock_with(|_| f).await
127
+ }
128
+
129
+ /// Lock, passing a [`RenderGuard`] witness into the task builder. The
130
+ /// task future is CONSTRUCTED after the lock is acquired — a guard can
131
+ /// never exist outside a locked section.
132
+ pub async fn lock_with<T, F, Fut>(&self, f: F) -> T
133
+ where
134
+ F: FnOnce(RenderGuard) -> Fut,
135
+ Fut: Future<Output = T>,
136
+ {
137
+ let guard = self.0.mutex.lock().await;
138
+ let held = HeldFlag::set(&self.0.held);
139
+ let result = f(RenderGuard { _private: () }).await;
140
+ drop(held);
141
+ drop(guard);
40
142
  result
41
143
  }
42
144
 
43
145
  /// Lock and also debounce `f`, which should be cancellable.
44
146
  pub async fn debounce(&self, f: impl Future<Output = ApiResult<()>>) -> ApiResult<()> {
45
- let next = self.0.id.get() + 1;
46
- let mut last = self.0.mutex.lock().await;
47
- if *last < next {
48
- let next = self.0.id.get() + 1;
49
- self.0.id.set(next);
50
- let result = f.await;
147
+ self.debounce_with(|_| f).await
148
+ }
149
+
150
+ /// [`Self::debounce`] with a [`RenderGuard`] witness (see
151
+ pub async fn debounce_with<T, F, Fut>(&self, f: F) -> ApiResult<T>
152
+ where
153
+ T: Default,
154
+ F: FnOnce(RenderGuard) -> Fut,
155
+ Fut: Future<Output = ApiResult<T>>,
156
+ {
157
+ DebounceSlot {
158
+ mutex: self.clone(),
159
+ state: self.0.default_slot.clone(),
160
+ }
161
+ .debounce_with(f)
162
+ .await
163
+ }
164
+
165
+ /// A NEW debounce slot over this mutex: its tasks serialize against
166
+ /// every other locked task, but coalesce only among themselves.
167
+ pub fn slot(&self) -> DebounceSlot {
168
+ DebounceSlot {
169
+ mutex: self.clone(),
170
+ state: Default::default(),
171
+ }
172
+ }
173
+ }
174
+
175
+ /// One debounce KIND over a shared [`DebounceMutex`]. Execution is
176
+ /// exclusive across the whole mutex; coalescing (a caller resolving via a
177
+ /// parked runner instead of queueing) is scoped to the slot. A coalesced
178
+ /// caller's work must be subsumable by any same-slot runner that runs no
179
+ /// earlier than the call — which is why parameterized tasks sharing a slot
180
+ /// must read their parameters at RUN time (e.g. the [`Renderer`] geometry
181
+ /// command cell), never capture them at call time.
182
+ #[derive(Clone)]
183
+ pub struct DebounceSlot {
184
+ mutex: DebounceMutex,
185
+ state: Rc<SlotState>,
186
+ }
187
+
188
+ impl DebounceSlot {
189
+ pub async fn debounce_with<T, F, Fut>(&self, f: F) -> ApiResult<T>
190
+ where
191
+ T: Default,
192
+ F: FnOnce(RenderGuard) -> Fut,
193
+ Fut: Future<Output = ApiResult<T>>,
194
+ {
195
+ let state = &self.state;
196
+ let next = state.id.get() + 1;
197
+ if state.parked.get() {
198
+ self.await_settled(next).await;
199
+ return Ok(T::default());
200
+ }
201
+
202
+ let settle = SettleGuard::park(state, next);
203
+ let guard = self.mutex.0.mutex.lock().await;
204
+ state.parked.set(false);
205
+ let result = if state.last.get() < next {
206
+ let next = state.id.get() + 1;
207
+ state.id.set(next);
208
+ let held = HeldFlag::set(&self.mutex.0.held);
209
+ let result = f(RenderGuard { _private: () }).await;
210
+ drop(held);
51
211
  if result.is_ok() {
52
- *last = next;
212
+ state.last.set(next);
53
213
  }
54
214
 
55
215
  result
56
216
  } else {
57
- Ok(())
217
+ Ok(T::default())
218
+ };
219
+
220
+ drop(guard);
221
+ drop(settle);
222
+ result
223
+ }
224
+
225
+ async fn await_settled(&self, next: u64) {
226
+ while self.state.settled_id.get() < next {
227
+ if self.state.on_settle.read_next().await.is_err() {
228
+ break;
229
+ }
58
230
  }
59
231
  }
60
232
  }
@@ -17,27 +17,33 @@
17
17
  //! reference other `crate::utils` modules when it helps reduce boiler-plate.
18
18
 
19
19
  mod browser;
20
+ mod completion;
20
21
  mod custom_element;
21
22
  mod datetime;
22
23
  mod debounce;
23
24
  mod hooks;
24
25
  mod modal_position;
25
26
  mod number_format;
27
+ mod pending_effects;
26
28
  mod ptr_eq_rc;
27
29
  mod pubsub;
30
+ mod spawn;
28
31
  mod weak_scope;
29
32
 
30
33
  #[cfg(test)]
31
34
  mod tests;
32
35
 
33
36
  pub use browser::*;
37
+ pub use completion::*;
34
38
  pub use custom_element::*;
35
39
  pub use datetime::*;
36
40
  pub use debounce::*;
37
41
  pub use hooks::*;
38
42
  pub use modal_position::*;
39
43
  pub use number_format::*;
44
+ pub use pending_effects::*;
40
45
  pub use perspective_client::clone;
41
46
  pub use ptr_eq_rc::*;
42
47
  pub use pubsub::*;
48
+ pub use spawn::*;
43
49
  pub use weak_scope::*;