@perspective-dev/viewer 4.5.1 → 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 (212) 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.cdn.d.ts +3 -0
  18. package/dist/esm/perspective-viewer.d.ts +9 -0
  19. package/dist/esm/perspective-viewer.inline.d.ts +3 -0
  20. package/dist/esm/perspective-viewer.inline.js +20 -1
  21. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  22. package/dist/esm/perspective-viewer.js +20 -1
  23. package/dist/esm/perspective-viewer.js.map +4 -4
  24. package/dist/esm/plugin.d.ts +71 -5
  25. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  26. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  27. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  28. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  29. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  30. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  31. package/dist/esm/ts-rs/OnUpdateData.d.ts +6 -0
  32. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  33. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  34. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  35. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  36. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  37. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
  38. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  39. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  40. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  41. package/dist/wasm/perspective-viewer.d.ts +213 -79
  42. package/dist/wasm/perspective-viewer.js +537 -189
  43. package/dist/wasm/perspective-viewer.wasm +0 -0
  44. package/dist/wasm/perspective-viewer.wasm.d.ts +36 -28
  45. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  46. package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  47. package/package.json +3 -2
  48. package/src/css/_column_dropdown.css +14 -0
  49. package/src/css/_context_menu.css +14 -0
  50. package/src/css/_dropdown_menu.css +14 -0
  51. package/src/css/_filter_dropdown.css +14 -0
  52. package/src/css/_function_dropdown.css +14 -0
  53. package/src/css/_viewer_bundle.css +41 -0
  54. package/src/css/column-settings-panel.css +1 -1
  55. package/src/css/containers/context-menu.css +101 -0
  56. package/src/css/panel-tab.css +193 -0
  57. package/src/css/plugin-selector.css +7 -10
  58. package/src/css/status-bar.css +75 -81
  59. package/src/css/viewer.css +181 -71
  60. package/src/rust/components/column_dropdown.rs +43 -33
  61. package/src/rust/components/column_selector/active_column.rs +7 -18
  62. package/src/rust/components/column_selector/aggregate_selector.rs +4 -10
  63. package/src/rust/components/column_selector/config_selector.rs +33 -84
  64. package/src/rust/components/column_selector/empty_column.rs +0 -3
  65. package/src/rust/components/column_selector/filter_column.rs +5 -18
  66. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  67. package/src/rust/components/column_selector/invalid_column.rs +0 -4
  68. package/src/rust/components/column_selector/sort_column.rs +3 -6
  69. package/src/rust/components/column_selector.rs +5 -14
  70. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +0 -3
  71. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +0 -3
  72. package/src/rust/components/column_settings_sidebar.rs +0 -3
  73. package/src/rust/components/containers/dropdown_menu.rs +1 -3
  74. package/src/rust/components/containers/scroll_panel.rs +1 -3
  75. package/src/rust/components/containers/split_panel.rs +175 -66
  76. package/src/rust/components/containers/tab_list.rs +0 -3
  77. package/src/rust/components/context_menu.rs +130 -0
  78. package/src/rust/components/datetime_column_style.rs +0 -3
  79. package/src/rust/components/empty_row.rs +0 -3
  80. package/src/rust/components/expression_editor.rs +2 -3
  81. package/src/rust/components/filter_dropdown.rs +3 -3
  82. package/src/rust/components/font_loader.rs +2 -2
  83. package/src/rust/components/form/code_editor.rs +0 -3
  84. package/src/rust/components/form/debug.rs +3 -4
  85. package/src/rust/components/function_dropdown.rs +3 -3
  86. package/src/rust/components/global_filter_bar.rs +76 -0
  87. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  88. package/src/rust/components/main_panel/msg.rs +62 -0
  89. package/src/rust/components/main_panel/presize.rs +220 -0
  90. package/src/rust/components/main_panel/reconcile.rs +224 -0
  91. package/src/rust/components/main_panel/render.rs +385 -0
  92. package/src/rust/components/main_panel/update.rs +184 -0
  93. package/src/rust/components/main_panel.rs +311 -71
  94. package/src/rust/components/mod.rs +4 -0
  95. package/src/rust/components/number_series_style.rs +0 -3
  96. package/src/rust/components/panel_menu.rs +486 -0
  97. package/src/rust/components/panel_tab.rs +505 -0
  98. package/src/rust/components/plugin_selector.rs +0 -3
  99. package/src/rust/components/plugin_tab.rs +1 -6
  100. package/src/rust/components/portal.rs +5 -2
  101. package/src/rust/components/render_warning.rs +0 -3
  102. package/src/rust/components/settings_panel.rs +13 -5
  103. package/src/rust/components/status_bar.rs +113 -180
  104. package/src/rust/components/status_indicator.rs +6 -9
  105. package/src/rust/components/string_column_style.rs +0 -3
  106. package/src/rust/components/style/mod.rs +65 -27
  107. package/src/rust/components/style/style_provider.rs +51 -16
  108. package/src/rust/components/style_controls/number_string_format.rs +1 -8
  109. package/src/rust/components/type_icon.rs +1 -4
  110. package/src/rust/components/viewer/filters.rs +133 -0
  111. package/src/rust/components/viewer/msg.rs +180 -0
  112. package/src/rust/components/viewer/panels.rs +326 -0
  113. package/src/rust/components/viewer/render.rs +243 -0
  114. package/src/rust/components/viewer/settings.rs +402 -0
  115. package/src/rust/components/viewer/snapshots.rs +134 -0
  116. package/src/rust/components/viewer/wiring.rs +505 -0
  117. package/src/rust/components/viewer.rs +243 -758
  118. package/src/rust/config/export_method.rs +1 -1
  119. package/src/rust/config/mod.rs +4 -0
  120. package/src/rust/config/options.rs +71 -0
  121. package/src/rust/config/viewer_config.rs +24 -2
  122. package/src/rust/config/workspace_config.rs +92 -0
  123. package/src/rust/custom_elements/copy_dropdown.rs +9 -3
  124. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  125. package/src/rust/custom_elements/export_dropdown.rs +9 -3
  126. package/src/rust/custom_elements/viewer.rs +1015 -286
  127. package/src/rust/custom_events.rs +212 -61
  128. package/src/rust/js/mod.rs +2 -0
  129. package/src/rust/js/plugin.rs +297 -10
  130. package/src/rust/js/regular_layout.rs +258 -0
  131. package/src/rust/lib.rs +20 -2
  132. package/src/rust/presentation.rs +66 -33
  133. package/src/rust/queries/get_viewer_config.rs +29 -11
  134. package/src/rust/queries/plugin_column_styles.rs +2 -2
  135. package/src/rust/renderer/activate.rs +41 -14
  136. package/src/rust/renderer/dispatch.rs +436 -0
  137. package/src/rust/renderer/plugin_config.rs +495 -0
  138. package/src/rust/renderer/registry.rs +18 -6
  139. package/src/rust/renderer/render_timer.rs +29 -10
  140. package/src/rust/renderer.rs +350 -827
  141. package/src/rust/session/column_defaults_update.rs +26 -8
  142. package/src/rust/session/metadata.rs +11 -1
  143. package/src/rust/session/view_subscription.rs +30 -15
  144. package/src/rust/session.rs +753 -251
  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/eject.rs +64 -23
  152. package/src/rust/tasks/mod.rs +28 -4
  153. package/src/rust/tasks/pipeline.rs +411 -0
  154. package/src/rust/tasks/presize_panels.rs +324 -0
  155. package/src/rust/tasks/reset_all.rs +22 -12
  156. package/src/rust/tasks/resize_observer.rs +124 -87
  157. package/src/rust/tasks/restore_and_render.rs +80 -86
  158. package/src/rust/tasks/restore_panel.rs +145 -0
  159. package/src/rust/tasks/send_column_config.rs +5 -2
  160. package/src/rust/tasks/send_plugin_config.rs +5 -2
  161. package/src/rust/tasks/set_edit_mode.rs +59 -0
  162. package/src/rust/tasks/sync_update_panels.rs +152 -0
  163. package/src/rust/tasks/table_lifecycle.rs +158 -0
  164. package/src/rust/tasks/update_theme.rs +56 -12
  165. package/src/rust/utils/completion.rs +56 -0
  166. package/src/rust/utils/custom_element.rs +5 -0
  167. package/src/rust/utils/debounce.rs +130 -7
  168. package/src/rust/utils/mod.rs +6 -0
  169. package/src/rust/utils/pending_effects.rs +62 -0
  170. package/src/rust/{components/style/local_style.rs → utils/spawn.rs} +17 -19
  171. package/src/rust/utils/weak_scope.rs +0 -10
  172. package/src/rust/workspace.rs +846 -0
  173. package/src/svg/active-indicator.svg +3 -0
  174. package/src/themes/botanical.css +7 -2
  175. package/src/themes/defaults.css +54 -2
  176. package/src/themes/dracula.css +8 -3
  177. package/src/themes/gruvbox-dark.css +7 -2
  178. package/src/themes/gruvbox.css +5 -2
  179. package/src/themes/icons.css +4 -2
  180. package/src/themes/monokai.css +7 -2
  181. package/src/themes/phosphor.css +7 -2
  182. package/src/themes/pro-dark.css +7 -2
  183. package/src/themes/pro.css +2 -1
  184. package/src/themes/solarized-dark.css +5 -2
  185. package/src/themes/solarized.css +5 -2
  186. package/src/themes/vaporwave.css +7 -2
  187. package/src/ts/bootstrap.ts +8 -7
  188. package/src/ts/extensions.ts +30 -2
  189. package/src/ts/perspective-viewer.ts +10 -0
  190. package/src/ts/plugin.ts +72 -5
  191. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  192. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  193. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  194. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  195. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  196. package/src/ts/ts-rs/Layout.ts +9 -0
  197. package/src/ts/ts-rs/Orientation.ts +6 -0
  198. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  199. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  200. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  201. package/src/ts/ts-rs/TabLayout.ts +11 -0
  202. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  203. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  204. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  205. package/tsconfig.json +1 -1
  206. package/src/rust/components/style/style_cache.rs +0 -136
  207. package/src/rust/tasks/intersection_observer.rs +0 -98
  208. package/src/rust/tasks/update_and_render.rs +0 -110
  209. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  210. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  211. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  212. /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline4.js → perspective-viewer-daeef4ceb066e6c7/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
  }
@@ -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,167 @@ 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
+
20
36
  #[derive(Default)]
21
37
  struct DebounceMutexData {
22
38
  id: Cell<u64>,
39
+ held: Cell<bool>,
23
40
  mutex: Mutex<u64>,
41
+ parked: Cell<bool>,
42
+ settled_id: Cell<u64>,
43
+ on_settle: PubSub<()>,
44
+ }
45
+
46
+ /// Clears the `held` flag on drop, so cancellation of a locked task (its
47
+ /// future dropped mid-await) can't leave [`DebounceMutex::is_held`] stuck
48
+ /// `true`.
49
+ struct HeldFlag<'a>(&'a Cell<bool>);
50
+
51
+ impl<'a> HeldFlag<'a> {
52
+ fn set(cell: &'a Cell<bool>) -> Self {
53
+ cell.set(true);
54
+ Self(cell)
55
+ }
56
+ }
57
+
58
+ impl Drop for HeldFlag<'_> {
59
+ fn drop(&mut self) {
60
+ self.0.set(false);
61
+ }
62
+ }
63
+
64
+ /// RAII for the parked debounce runner: clears `parked` and broadcasts the
65
+ /// runner's settle on drop. Running through `Drop` (not a happy-path call)
66
+ /// makes the release unconditional — a runner cancelled mid-park or
67
+ /// mid-run still frees the parked slot and releases its coalesced waiters,
68
+ /// so they can never be stranded awaiting a settle that no task will emit.
69
+ struct SettleGuard<'a> {
70
+ data: &'a DebounceMutexData,
71
+ next: u64,
72
+ }
73
+
74
+ impl<'a> SettleGuard<'a> {
75
+ fn park(data: &'a DebounceMutexData, next: u64) -> Self {
76
+ data.parked.set(true);
77
+ Self { data, next }
78
+ }
79
+ }
80
+
81
+ impl Drop for SettleGuard<'_> {
82
+ fn drop(&mut self) {
83
+ self.data.parked.set(false);
84
+ if self.data.settled_id.get() < self.next {
85
+ self.data.settled_id.set(self.next);
86
+ }
87
+
88
+ self.data.on_settle.emit(());
89
+ }
24
90
  }
25
91
 
26
92
  /// 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.
93
+ /// debounces calls in addition to providing exclusivity. Calling
94
+ /// [`Self::debounce`] resolves only after at least one complete evaluation
95
+ /// of a call that began no earlier than this one — either by running the
96
+ /// caller's own future, or by coalescing onto the single parked trailing
97
+ /// runner. At most TWO debounce evaluations are ever outstanding (one
98
+ /// running, one parked); every additional concurrent caller resolves
99
+ /// without queueing on the lock.
30
100
  #[derive(Clone, Default)]
31
101
  pub struct DebounceMutex(Rc<DebounceMutexData>);
32
102
 
33
103
  impl DebounceMutex {
104
+ /// `true` while a locked task is executing. Used by lock-acquiring
105
+ /// public API methods to emit a debug-build warning when they are about
106
+ /// to queue behind an in-flight run — legitimate for app callers, a
107
+ /// guaranteed deadlock when reached synchronously from a plugin's render
108
+ /// (the render-callable contract on `js::plugin` forbids it).
109
+ pub fn is_held(&self) -> bool {
110
+ self.0.held.get()
111
+ }
112
+
34
113
  /// Lock like a normal `Mutex`.
35
114
  pub async fn lock<T>(&self, f: impl Future<Output = T>) -> T {
115
+ self.lock_with(|_| f).await
116
+ }
117
+
118
+ /// Lock, passing a [`RenderGuard`] witness into the task builder. The
119
+ /// task future is CONSTRUCTED after the lock is acquired — a guard can
120
+ /// never exist outside a locked section.
121
+ pub async fn lock_with<T, F, Fut>(&self, f: F) -> T
122
+ where
123
+ F: FnOnce(RenderGuard) -> Fut,
124
+ Fut: Future<Output = T>,
125
+ {
36
126
  let mut last = self.0.mutex.lock().await;
37
127
  let next = self.0.id.get();
38
- let result = f.await;
128
+ let held = HeldFlag::set(&self.0.held);
129
+ let result = f(RenderGuard { _private: () }).await;
130
+ drop(held);
39
131
  *last = next;
40
132
  result
41
133
  }
42
134
 
43
135
  /// Lock and also debounce `f`, which should be cancellable.
44
136
  pub async fn debounce(&self, f: impl Future<Output = ApiResult<()>>) -> ApiResult<()> {
137
+ self.debounce_with(|_| f).await
138
+ }
139
+
140
+ /// [`Self::debounce`] with a [`RenderGuard`] witness (see
141
+ pub async fn debounce_with<T, F, Fut>(&self, f: F) -> ApiResult<T>
142
+ where
143
+ T: Default,
144
+ F: FnOnce(RenderGuard) -> Fut,
145
+ Fut: Future<Output = ApiResult<T>>,
146
+ {
45
147
  let next = self.0.id.get() + 1;
148
+ if self.0.parked.get() {
149
+ self.await_settled(next).await;
150
+ return Ok(T::default());
151
+ }
152
+
153
+ let settle = SettleGuard::park(&self.0, next);
46
154
  let mut last = self.0.mutex.lock().await;
47
- if *last < next {
155
+ self.0.parked.set(false);
156
+ let result = if *last < next {
48
157
  let next = self.0.id.get() + 1;
49
158
  self.0.id.set(next);
50
- let result = f.await;
159
+ let held = HeldFlag::set(&self.0.held);
160
+ let result = f(RenderGuard { _private: () }).await;
161
+ drop(held);
51
162
  if result.is_ok() {
52
163
  *last = next;
53
164
  }
54
165
 
55
166
  result
56
167
  } else {
57
- Ok(())
168
+ Ok(T::default())
169
+ };
170
+
171
+ drop(last);
172
+ drop(settle);
173
+ result
174
+ }
175
+
176
+ async fn await_settled(&self, next: u64) {
177
+ while self.0.settled_id.get() < next {
178
+ if self.0.on_settle.read_next().await.is_err() {
179
+ break;
180
+ }
58
181
  }
59
182
  }
60
183
  }
@@ -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::*;
@@ -0,0 +1,62 @@
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 super::pubsub::PubSub;
17
+
18
+ /// The element-scoped ledger of in-flight EFFECTS: every effectful public
19
+ /// method (and the internal flows they schedule, e.g. the `before-resize`
20
+ /// presize) registers an [`EffectGuard`] for the duration of its async
21
+ /// work, whether or not the caller awaited it. `flush()` resolves only
22
+ /// after the ledger drains, so "called before `flush`" implies "applied
23
+ /// before `flush` resolves".
24
+ #[derive(Clone, Default)]
25
+ pub struct EffectLedger(Rc<EffectLedgerData>);
26
+
27
+ #[derive(Default)]
28
+ struct EffectLedgerData {
29
+ count: Cell<u32>,
30
+ on_drain: PubSub<()>,
31
+ }
32
+
33
+ pub struct EffectGuard(EffectLedger);
34
+
35
+ impl EffectLedger {
36
+ pub fn guard(&self) -> EffectGuard {
37
+ self.0.count.set(self.0.count.get() + 1);
38
+ EffectGuard(self.clone())
39
+ }
40
+
41
+ pub fn is_empty(&self) -> bool {
42
+ self.0.count.get() == 0
43
+ }
44
+
45
+ pub async fn settle(&self) {
46
+ while self.0.count.get() > 0 {
47
+ if self.0.on_drain.read_next().await.is_err() {
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ impl Drop for EffectGuard {
55
+ fn drop(&mut self) {
56
+ let data = &self.0.0;
57
+ data.count.set(data.count.get() - 1);
58
+ if data.count.get() == 0 {
59
+ data.on_drain.emit(());
60
+ }
61
+ }
62
+ }
@@ -10,26 +10,24 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use yew::prelude::*;
13
+ use std::future::Future;
14
14
 
15
- use super::style_cache::*;
15
+ use perspective_js::utils::*;
16
16
 
17
- #[derive(Properties)]
18
- pub struct LocalStyleProps {
19
- pub href: (&'static str, &'static str),
20
- }
21
-
22
- impl PartialEq for LocalStyleProps {
23
- fn eq(&self, _other: &Self) -> bool {
24
- true
25
- }
26
- }
27
-
28
- #[function_component(LocalStyle)]
29
- pub fn local_style(props: &LocalStyleProps) -> Html {
30
- if let Some(cache) = use_context::<StyleCache>() {
31
- cache.add_style(props.href.0, props.href.1);
32
- }
17
+ /// Spawn a fire-and-forget task whose ERRORS ARE OWNED: a failure lands in a
18
+ /// `tracing::error` tagged with `tag` instead of a dangling `Promise`
19
+ /// rejection (which `ApiFuture::spawn` produces, invisibly to callers and
20
+ /// tests). "View already deleted" cancellations are not failures.
21
+ ///
22
+ /// Prefer RETURNING the future so the caller owns completion and error
23
+ /// (invariant I6); use this only at event-listener leaves where no caller
24
+ /// exists.
25
+ pub fn spawn_owned(tag: &'static str, fut: impl Future<Output = ApiResult<()>> + 'static) {
26
+ ApiFuture::spawn(async move {
27
+ if let Err(e) = fut.await.ignore_view_delete() {
28
+ tracing::error!("[{}] unhandled task error: {}", tag, e);
29
+ }
33
30
 
34
- html! {}
31
+ Ok(())
32
+ });
35
33
  }
@@ -37,13 +37,3 @@ impl<C: Component> PartialEq for WeakScope<C> {
37
37
  Rc::ptr_eq(&self.0, &other.0)
38
38
  }
39
39
  }
40
-
41
- // #[extend::ext]
42
- // pub impl<T: Component> Context<T> {
43
- // fn context<U: Clone + PartialEq + 'static>(&self) -> U {
44
- // self.link()
45
- // .context::<U>(Callback::from(|_| {}))
46
- // .map(|(c, _)| c)
47
- // .unwrap()
48
- // }
49
- // }