@perspective-dev/viewer 5.5.1 → 5.6.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 (194) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -0,0 +1,327 @@
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::rc::Rc;
14
+
15
+ use perspective_client::config::{ColumnType, Filter, ViewConfig};
16
+ use perspective_client::{Client, Description, Table};
17
+
18
+ use super::TableErrorState;
19
+ use super::metadata::SessionMetadata;
20
+
21
+ /// One panel's committed state, as ONE immutable value: a holder of an
22
+ /// `Rc<PanelState>` sees a consistent snapshot forever, and a change is a new
23
+ /// value swapped in whole.
24
+ #[derive(Clone, Default)]
25
+ pub struct PanelState {
26
+ pub chrome: Chrome,
27
+ pub binding: Binding,
28
+
29
+ /// The element's global filter as broadcast to this panel — the WHOLE set.
30
+ pub overlay: Rc<Vec<OverlayClause>>,
31
+ }
32
+
33
+ /// One clause of the element-level global filter, with the type its column has
34
+ /// in the panel that BROADCAST it.
35
+ #[derive(Clone, Debug, PartialEq)]
36
+ pub struct OverlayClause {
37
+ pub filter: Filter,
38
+ pub column_type: Option<ColumnType>,
39
+ }
40
+
41
+ impl From<Filter> for OverlayClause {
42
+ /// A clause with no broadcaster.
43
+ fn from(filter: Filter) -> Self {
44
+ Self {
45
+ filter,
46
+ column_type: None,
47
+ }
48
+ }
49
+ }
50
+
51
+ /// The config a `View` is built from: `config`, plus each `overlay` clause the
52
+ /// listening table can honor — one naming a column the listener has, with the
53
+ /// broadcaster's type.
54
+ pub fn effective(
55
+ config: &ViewConfig,
56
+ overlay: &[OverlayClause],
57
+ type_of: &dyn Fn(&str) -> Option<ColumnType>,
58
+ ) -> (ViewConfig, Vec<usize>) {
59
+ let mut effective = config.clone();
60
+ let mut skipped = Vec::new();
61
+ for (idx, clause) in overlay.iter().enumerate() {
62
+ let applies = match (type_of(clause.filter.column()), clause.column_type) {
63
+ (Some(listener), Some(broadcaster)) => listener == broadcaster,
64
+ (Some(_), None) => true,
65
+ (None, _) => false,
66
+ };
67
+
68
+ if applies {
69
+ effective.filter.push(clause.filter.clone());
70
+ } else {
71
+ skipped.push(idx);
72
+ }
73
+ }
74
+
75
+ (effective, skipped)
76
+ }
77
+
78
+ /// Panel presentation that is not a function of the data.
79
+ #[derive(Clone, Default)]
80
+ pub struct Chrome {
81
+ pub title: Option<String>,
82
+ }
83
+
84
+ /// What this panel is bound to.
85
+ #[derive(Clone)]
86
+ pub enum Binding {
87
+ /// No table requested.
88
+ Unbound {
89
+ client: Option<Client>,
90
+ config: Rc<ViewConfig>,
91
+ },
92
+
93
+ /// A named table that no client hosts yet; `client` is the one asked.
94
+ Awaiting {
95
+ client: Client,
96
+ name: String,
97
+ config: Rc<ViewConfig>,
98
+ },
99
+
100
+ /// Bound to `table`.
101
+ Bound(Rc<BoundState>),
102
+
103
+ /// The binding failed or its connection dropped.
104
+ Lost {
105
+ prior: Rc<Binding>,
106
+ error: TableErrorState,
107
+ },
108
+ }
109
+
110
+ impl Default for Binding {
111
+ fn default() -> Self {
112
+ Self::Unbound {
113
+ client: None,
114
+ config: Rc::default(),
115
+ }
116
+ }
117
+ }
118
+
119
+ #[derive(Clone)]
120
+ pub struct BoundState {
121
+ pub table: Table,
122
+ pub metadata: Rc<SessionMetadata>,
123
+ pub config: Rc<ViewConfig>,
124
+
125
+ /// The server's [`Description`] of the last validated effective config,
126
+ /// keyed by that config.
127
+ pub description: Option<(Rc<ViewConfig>, Rc<Description>)>,
128
+ }
129
+
130
+ impl Binding {
131
+ /// This binding, seen through a [`Binding::Lost`] to what it was.
132
+ fn live(&self) -> &Binding {
133
+ match self {
134
+ Binding::Lost { prior, .. } => prior,
135
+ binding => binding,
136
+ }
137
+ }
138
+
139
+ /// `f` applied to the live binding, preserving a `Lost` wrapper.
140
+ fn map_live(&self, f: impl FnOnce(&Binding) -> Binding) -> Binding {
141
+ match self {
142
+ Binding::Lost { prior, error } => Binding::Lost {
143
+ prior: Rc::new(f(prior)),
144
+ error: error.clone(),
145
+ },
146
+ binding => f(binding),
147
+ }
148
+ }
149
+ }
150
+
151
+ impl PanelState {
152
+ pub fn config(&self) -> &Rc<ViewConfig> {
153
+ match self.binding.live() {
154
+ Binding::Unbound { config, .. } | Binding::Awaiting { config, .. } => config,
155
+ Binding::Bound(bound) => &bound.config,
156
+ Binding::Lost { .. } => unreachable!("`Lost::prior` is never `Lost`"),
157
+ }
158
+ }
159
+
160
+ /// The client this panel is bound through, awaits a table on, or will look
161
+ /// one up on.
162
+ pub fn client(&self) -> Option<Client> {
163
+ match self.binding.live() {
164
+ Binding::Unbound { client, .. } => client.clone(),
165
+ Binding::Awaiting { client, .. } => Some(client.clone()),
166
+ Binding::Bound(bound) => Some(bound.table.get_client()),
167
+ Binding::Lost { .. } => unreachable!("`Lost::prior` is never `Lost`"),
168
+ }
169
+ }
170
+
171
+ pub fn bound(&self) -> Option<&Rc<BoundState>> {
172
+ match self.binding.live() {
173
+ Binding::Bound(bound) => Some(bound),
174
+ _ => None,
175
+ }
176
+ }
177
+
178
+ pub fn awaiting(&self) -> Option<&str> {
179
+ match self.binding.live() {
180
+ Binding::Awaiting { name, .. } => Some(name),
181
+ _ => None,
182
+ }
183
+ }
184
+
185
+ pub fn lost(&self) -> Option<&TableErrorState> {
186
+ match &self.binding {
187
+ Binding::Lost { error, .. } => Some(error),
188
+ _ => None,
189
+ }
190
+ }
191
+
192
+ /// This state with its binding lost to `error`.
193
+ pub fn with_lost(&self, error: TableErrorState) -> Self {
194
+ Self {
195
+ binding: Binding::Lost {
196
+ prior: Rc::new(self.binding.live().clone()),
197
+ error,
198
+ },
199
+ ..self.clone()
200
+ }
201
+ }
202
+
203
+ /// This state with a lost binding recovered to what it was.
204
+ pub fn recovered(&self) -> Self {
205
+ Self {
206
+ binding: self.binding.live().clone(),
207
+ ..self.clone()
208
+ }
209
+ }
210
+
211
+ /// This state with `config` in place of its view config.
212
+ pub fn with_config(&self, config: Rc<ViewConfig>) -> Self {
213
+ let binding = self.binding.map_live(|binding| match binding {
214
+ Binding::Unbound { client, .. } => Binding::Unbound {
215
+ client: client.clone(),
216
+ config,
217
+ },
218
+ Binding::Awaiting { client, name, .. } => Binding::Awaiting {
219
+ client: client.clone(),
220
+ name: name.clone(),
221
+ config,
222
+ },
223
+ Binding::Bound(bound) => Binding::Bound(Rc::new(BoundState {
224
+ config,
225
+ description: None,
226
+ ..(**bound).clone()
227
+ })),
228
+ Binding::Lost { .. } => unreachable!("`Lost::prior` is never `Lost`"),
229
+ });
230
+
231
+ Self {
232
+ binding,
233
+ ..self.clone()
234
+ }
235
+ }
236
+
237
+ /// This state with nothing bound and nothing awaited (a lost binding is
238
+ /// replaced, not recovered); the client and config are kept.
239
+ pub fn unbound(&self) -> Self {
240
+ self.unbound_on(self.client())
241
+ }
242
+
243
+ /// This state with nothing bound, looking tables up on `client`.
244
+ pub fn unbound_on(&self, client: Option<Client>) -> Self {
245
+ Self {
246
+ binding: Binding::Unbound {
247
+ client,
248
+ config: self.config().clone(),
249
+ },
250
+ ..self.clone()
251
+ }
252
+ }
253
+
254
+ /// This state awaiting a host for `name` on `client`; the config is kept.
255
+ pub fn awaiting_table(&self, client: Client, name: String) -> Self {
256
+ Self {
257
+ binding: Binding::Awaiting {
258
+ client,
259
+ name,
260
+ config: self.config().clone(),
261
+ },
262
+ ..self.clone()
263
+ }
264
+ }
265
+
266
+ /// This state with `overlay` broadcast to it.
267
+ pub fn with_overlay(&self, overlay: Rc<Vec<OverlayClause>>) -> Self {
268
+ Self {
269
+ overlay,
270
+ ..self.with_description(None)
271
+ }
272
+ }
273
+
274
+ /// This state bound to `table`; the config is kept and nothing has been
275
+ /// described against the new table yet.
276
+ pub fn bound_to(&self, table: Table, metadata: SessionMetadata) -> Self {
277
+ Self {
278
+ binding: Binding::Bound(Rc::new(BoundState {
279
+ table,
280
+ metadata: Rc::new(metadata),
281
+ config: self.config().clone(),
282
+ description: None,
283
+ })),
284
+ ..self.clone()
285
+ }
286
+ }
287
+
288
+ /// This state with `metadata` in place of its table metadata; a no-op
289
+ /// unless bound.
290
+ pub fn with_metadata(&self, metadata: Rc<SessionMetadata>) -> Self {
291
+ let binding = self.binding.map_live(|binding| match binding {
292
+ Binding::Bound(bound) => Binding::Bound(Rc::new(BoundState {
293
+ metadata,
294
+ ..(**bound).clone()
295
+ })),
296
+ binding => binding.clone(),
297
+ });
298
+
299
+ Self {
300
+ binding,
301
+ ..self.clone()
302
+ }
303
+ }
304
+
305
+ /// This state with `description` recorded; a no-op unless bound.
306
+ pub fn with_description(&self, description: Option<(Rc<ViewConfig>, Rc<Description>)>) -> Self {
307
+ let binding = self.binding.map_live(|binding| match binding {
308
+ Binding::Bound(bound) => Binding::Bound(Rc::new(BoundState {
309
+ description,
310
+ ..(**bound).clone()
311
+ })),
312
+ binding => binding.clone(),
313
+ });
314
+
315
+ Self {
316
+ binding,
317
+ ..self.clone()
318
+ }
319
+ }
320
+
321
+ pub fn with_title(&self, title: Option<String>) -> Self {
322
+ Self {
323
+ chrome: Chrome { title },
324
+ ..self.clone()
325
+ }
326
+ }
327
+ }
@@ -43,6 +43,11 @@ struct ViewSubscriptionData {
43
43
  on_update: Option<Callback<()>>,
44
44
  is_deleted: Rc<Cell<bool>>,
45
45
 
46
+ /// Set by [`ViewSubscription::quiesce`]: the engine registration is gone
47
+ /// and BOTH emit channels are dead, including any update already in
48
+ /// flight when the flag flipped.
49
+ quiesced: Rc<Cell<bool>>,
50
+
46
51
  /// The bound `View`'s latest known dimensions, written before
47
52
  /// `on_update`/`on_stats` fire.
48
53
  dimensions: Rc<RefCell<Option<ViewDimensionsResp>>>,
@@ -57,8 +62,14 @@ pub struct ViewSubscription {
57
62
  impl ViewSubscriptionData {
58
63
  /// Main handler when underlying `View()` calls `on_update()`.
59
64
  async fn on_view_update(self) -> ApiResult<JsValue> {
65
+ if self.quiesced.get() {
66
+ return Ok(JsValue::UNDEFINED);
67
+ }
68
+
60
69
  self.clone().update_view_stats().await?;
61
- if let Some(on_update) = &self.on_update {
70
+ if let Some(on_update) = &self.on_update
71
+ && !self.quiesced.get()
72
+ {
62
73
  on_update.emit(());
63
74
  };
64
75
 
@@ -66,6 +77,10 @@ impl ViewSubscriptionData {
66
77
  }
67
78
 
68
79
  async fn update_view_stats(self) -> ApiResult<JsValue> {
80
+ if self.quiesced.get() {
81
+ return Ok(JsValue::UNDEFINED);
82
+ }
83
+
69
84
  let dimensions = self.view.dimensions().await?;
70
85
  let num_rows = dimensions.num_table_rows;
71
86
  let num_cols = dimensions.num_table_columns;
@@ -80,7 +95,10 @@ impl ViewSubscriptionData {
80
95
  is_split_by: !self.config.split_by.is_empty(),
81
96
  };
82
97
 
83
- self.on_stats.emit(stats);
98
+ if !self.quiesced.get() {
99
+ self.on_stats.emit(stats);
100
+ }
101
+
84
102
  Ok(JsValue::UNDEFINED)
85
103
  }
86
104
 
@@ -90,7 +108,7 @@ impl ViewSubscriptionData {
90
108
  }
91
109
 
92
110
  let view = &self.view;
93
- if self.on_update.is_some() {
111
+ if self.on_update.is_some() && !self.quiesced.get() {
94
112
  view.remove_update(self.callback_id.get()).await?;
95
113
  }
96
114
 
@@ -125,6 +143,7 @@ impl ViewSubscription {
125
143
  callback_id: Rc::default(),
126
144
  on_update,
127
145
  is_deleted: Rc::default(),
146
+ quiesced: Rc::default(),
128
147
  dimensions: Rc::default(),
129
148
  };
130
149
 
@@ -191,6 +210,26 @@ impl ViewSubscription {
191
210
  self.data.build_config.clone()
192
211
  }
193
212
 
213
+ /// Permanently detach this subscription from its `Session` — the engine
214
+ /// `on_update` registration removed and both emit channels silenced
215
+ /// (in-flight updates included) — leaving a bare keep-alive handle for a
216
+ /// `View` that now feeds a derived `Table` rather than this panel.
217
+ pub async fn quiesce(&self) -> ApiResult<()> {
218
+ if self.data.quiesced.get() {
219
+ return Ok(());
220
+ }
221
+
222
+ if self.data.on_update.is_some() {
223
+ self.data
224
+ .view
225
+ .remove_update(self.data.callback_id.get())
226
+ .await?;
227
+ }
228
+
229
+ self.data.quiesced.set(true);
230
+ Ok(())
231
+ }
232
+
194
233
  /// Delete this `View`. Neglecting to call this method before a
195
234
  /// `ViewSubscription` is dropped will result in a log warning, but the
196
235
  /// `View` will not leak.