@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,182 @@
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
+ //! The root component's message protocol. One flat enum — the `update()` match
14
+ //! in `viewer.rs` is its dispatch table; handler bodies live in the sibling
15
+ //! domain modules ([`super::panels`], [`super::settings`], [`super::filters`],
16
+ //! [`super::snapshots`]).
17
+
18
+ use futures::channel::oneshot::Sender;
19
+ use perspective_client::config::Filter;
20
+ use perspective_js::utils::ApiResult;
21
+ use wasm_bindgen::JsValue;
22
+
23
+ use crate::components::settings_panel::SelectedTab;
24
+ use crate::config::*;
25
+ use crate::presentation::{ColumnLocator, ColumnSettingsTab, DragDropProps, PresentationProps};
26
+ use crate::renderer::RendererProps;
27
+ use crate::session::{SessionProps, TableLoadState, ViewStats};
28
+ use crate::utils::Completion;
29
+
30
+ /// The filter-bearing payload of a master panel's selection or click event
31
+ /// (`MasterContribution`), as decoded by the host listeners in
32
+ /// [`super::wiring`].
33
+ #[derive(Debug)]
34
+ pub struct MasterSelection {
35
+ /// The event's filter clauses (`insertFilters` for the select-detail
36
+ /// family, `config.filter` for clicks) — BEFORE the master's own stored
37
+ /// filters are subtracted (see `on_master_contribution`).
38
+ pub filters: Vec<Filter>,
39
+
40
+ /// A synthesized clicked-cell `[column, "==", value]` clause, used when
41
+ /// `filters` derives to nothing — e.g. a FLAT (un-grouped) datagrid
42
+ /// master, whose clicks carry no group-by path to filter on.
43
+ pub cell_fallback: Option<Filter>,
44
+ }
45
+
46
+ #[derive(Debug)]
47
+ pub enum PerspectiveViewerMsg {
48
+ ColumnSettingsPanelSizeUpdate(Option<i32>),
49
+ ColumnSettingsPanelAutoWidth(f64),
50
+ ToggleColumnSettingsPin,
51
+ ColumnSettingsTabChanged(ColumnSettingsTab),
52
+ OpenColumnSettings {
53
+ locator: Option<ColumnLocator>,
54
+ sender: Option<Sender<()>>,
55
+ toggle: bool,
56
+ },
57
+ PreloadFontsUpdate,
58
+
59
+ /// Element-level reset (the public `reset()` API): reset EVERY panel and
60
+ /// clear the cross-filter overlay, symmetric with whole-element
61
+ /// `save`/`restore`. The `bool` also clears expressions/column settings.
62
+ Reset(bool, Option<Completion>),
63
+
64
+ /// Reset ONLY the named panel — or the active panel when `None` — to its
65
+ /// default `ViewerConfig` (the toolbar Reset button, the context menu's
66
+ /// "Reset" command, and the public `resetPanel()` API). The `bool` is
67
+ /// `Reset`'s expressions flag (toolbar shift-click); the `Completion`
68
+ /// resolves the `resetPanel()` promise after the reset's run completes
69
+ /// (invariant I6).
70
+ ResetPanel(Option<String>, bool, Option<Completion>),
71
+ Resize,
72
+
73
+ /// The set of layout panels changed (added/removed); re-render so the
74
+ /// layout host reconciles its `<regular-layout>` cells.
75
+ LayoutChanged,
76
+
77
+ /// Make the named panel active: re-target the settings panel + status bar
78
+ /// (and the root's session/renderer subscriptions) to its engines. The
79
+ /// `Completion` resolves `setActivePanel()` after the activation-chrome
80
+ /// nudge runs complete (invariant I6).
81
+ SetActivePanel(String, Option<Completion>),
82
+
83
+ /// The named panel's frame was closed (removed from the layout); remove it
84
+ /// from the workspace and dispose its engines. The `Completion` resolves
85
+ /// `removePanel()` after the eject's teardown run completes (invariant
86
+ /// I6) — carrying any teardown error, which was previously dropped.
87
+ ClosePanel(String, Option<Completion>),
88
+
89
+ /// Whole-element `restore` finished replacing the panel set in the
90
+ /// `Workspace` (new models inserted, old panels ejected, layout staged):
91
+ /// activate the named panel, re-subscribe the per-panel wiring, and
92
+ /// re-render — the SINGLE visible commit of the whole restore.
93
+ CommitWorkspaceRestore(String),
94
+
95
+ /// Duplicate the named panel: snapshot its config into a new independent
96
+ /// panel appended to the layout.
97
+ DuplicatePanel(String),
98
+
99
+ /// New panel: a fresh (default-config) panel bound to the named panel's
100
+ /// table (from the default client).
101
+ NewPanel(String),
102
+
103
+ /// New panel bound to the named `Table` on the named `Client` (the
104
+ /// context menu's "New" sub-menu). The `Client` is resolved by name from
105
+ /// the `Workspace` loaded-clients registry.
106
+ NewPanelFrom {
107
+ client: String,
108
+ table: String,
109
+ },
110
+
111
+ /// Toggle the named panel's master/detail (filter-source) role.
112
+ ToggleMaster(String),
113
+
114
+ /// A master panel's selection state, from EITHER host listener
115
+ /// (`perspective-global-filter` select/deselect or `perspective-click`):
116
+ /// `Some` REPLACES that panel's global-filter contribution, `None`
117
+ /// (deselect) clears it. Non-master sources are ignored by the handler.
118
+ MasterContribution(String, Option<MasterSelection>),
119
+
120
+ /// Remove the global filter at this index (GlobalFilterBar chip ×).
121
+ RemoveGlobalFilter(usize),
122
+
123
+ /// Clear all global filters (GlobalFilterBar "Clear").
124
+ ClearGlobalFilters,
125
+
126
+ /// Some panel's title changed (any panel, via `_title_subscriptions`);
127
+ /// re-render so the tab titles refresh.
128
+ TitlesChanged,
129
+ SettingsPanelSizeUpdate(Option<i32>),
130
+
131
+ /// The settings-pane divider proposed a new pane width (per pointermove,
132
+ /// from the *deferred* `SplitPanel` — it has NOT been applied). Feeds the
133
+ /// latest-wins presize pump (`PRESIZE_EVERYWHERE_PLAN.md` P1): geometry
134
+ /// commits only after every visible panel has rendered at its target.
135
+ SettingsDividerMove(i32),
136
+
137
+ /// Run one pump iteration: presize all visible panels at the newest
138
+ /// proposed pane width, then commit it.
139
+ SettingsDividerPump,
140
+
141
+ /// Presize for this pane width completed — commit it (the deferred
142
+ /// `SplitPanel`'s controlled `size`), then pump again if a newer target
143
+ /// arrived meanwhile.
144
+ SettingsDividerCommit(i32),
145
+
146
+ /// Divider drag ended: reactively finalize every visible panel at its
147
+ /// exact settled cell (debounced no-op when the presizes were exact).
148
+ SettingsDividerFinish,
149
+ SettingsPanelTabChanged(SelectedTab),
150
+ SettingsPanelAutoWidth(f64),
151
+ ToggleDebug,
152
+ ToggleSettingsComplete(SettingsUpdate, Sender<()>),
153
+ ToggleSettingsInit(Option<SettingsUpdate>, Option<Sender<ApiResult<JsValue>>>),
154
+ UpdateSession(Box<SessionProps>),
155
+ UpdateRenderer(Box<RendererProps>),
156
+ UpdatePresentation(Box<PresentationProps>),
157
+
158
+ /// Update only `is_settings_open` in the presentation snapshot without
159
+ /// touching `available_themes` (which requires async data).
160
+ UpdateSettingsOpen(bool),
161
+ UpdateIsWorkspace(bool),
162
+
163
+ /// Update only `open_column_settings` in the presentation snapshot.
164
+ UpdateColumnSettings(Box<crate::presentation::OpenColumnSettings>),
165
+ UpdateDragDrop(Box<DragDropProps>),
166
+
167
+ /// Update only stats-related fields of `session_props` without touching
168
+ /// `config`. This prevents `stats_changed` events (e.g. from `reset()`)
169
+ /// from propagating a freshly-cleared config to the column selector.
170
+ UpdateSessionStats(Option<ViewStats>, Option<TableLoadState>),
171
+
172
+ /// Refresh the root's render snapshot of the `Workspace`-owned global
173
+ /// filter set (dispatched by its `filters_changed` PubSub).
174
+ UpdateGlobalFilters,
175
+
176
+ /// The active panel's in-flight config-run count changed. LEVEL-
177
+ /// triggered: the payload is the ABSOLUTE count (RAII-settled — see
178
+ /// `Session::begin_config_run`), which the handler ASSIGNS to
179
+ /// `update_count`; there is no delta arithmetic to drift. Threaded to
180
+ /// `StatusIndicator` as the "updating" spinner.
181
+ UpdateInFlight(u32),
182
+ }
@@ -0,0 +1,326 @@
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
+ //! Panel-lifecycle handlers: the [`Workspace`] panel set (add / duplicate /
14
+ //! close) and active-panel targeting. INVARIANT: app-initiated layout changes
15
+ //! mutate the `Workspace` FIRST, synchronously — `regular-layout` is
16
+ //! reconciled as a downstream consequence of the re-render, never the source
17
+ //! of truth (see `ClosePanel`).
18
+ //!
19
+ //! [`Workspace`]: crate::workspace::Workspace
20
+
21
+ use perspective_js::utils::*;
22
+ use yew::prelude::*;
23
+
24
+ use super::PerspectiveViewer;
25
+ use super::msg::PerspectiveViewerMsg::*;
26
+ use super::wiring::{
27
+ clear_active_callbacks, create_active_subscriptions, inject_active_callbacks,
28
+ subscribe_panel_titles,
29
+ };
30
+ use crate::config::{TableUpdate, ViewerConfigUpdate};
31
+ use crate::queries::*;
32
+ use crate::renderer::Renderer;
33
+ use crate::session::*;
34
+ use crate::tasks::*;
35
+ use crate::utils::{Completion, spawn_owned};
36
+ use crate::workspace::PanelId;
37
+
38
+ impl PerspectiveViewer {
39
+ pub(super) fn on_layout_changed(&mut self, ctx: &Context<Self>) -> bool {
40
+ self._title_subscriptions = subscribe_panel_titles(ctx);
41
+ self.resync_active(ctx);
42
+ true
43
+ }
44
+
45
+ /// Level-triggered binding sync: re-point the root's engine bindings at
46
+ /// the model's *current* active panel (or the detached empty engines at
47
+ /// zero panels) when they differ, by `Rc` identity. Catches model-side
48
+ /// activations that never dispatch `SetActivePanel`.
49
+ fn resync_active(&mut self, ctx: &Context<Self>) -> bool {
50
+ let session = ctx
51
+ .props()
52
+ .workspace
53
+ .active_session()
54
+ .unwrap_or_else(|| self.empty_session.clone());
55
+ let renderer = ctx
56
+ .props()
57
+ .workspace
58
+ .active_renderer()
59
+ .unwrap_or_else(|| self.empty_renderer.clone());
60
+
61
+ if session == self.active_session && renderer == self.active_renderer {
62
+ false
63
+ } else {
64
+ self.retarget_active(ctx, session, renderer);
65
+ true
66
+ }
67
+ }
68
+
69
+ pub(super) fn on_set_active_panel(
70
+ &mut self,
71
+ ctx: &Context<Self>,
72
+ id: String,
73
+ completion: Option<Completion>,
74
+ ) -> bool {
75
+ let id = PanelId::from(id);
76
+ let prev = ctx.props().workspace.active_id();
77
+ if prev.as_ref() == Some(&id) || !ctx.props().workspace.set_active(id.clone()) {
78
+ let resynced = self.resync_active(ctx);
79
+ if let Some(completion) = completion {
80
+ completion.resolve_after(async { Ok(()) });
81
+ }
82
+
83
+ resynced
84
+ } else {
85
+ let new_session = ctx
86
+ .props()
87
+ .workspace
88
+ .active_session()
89
+ .unwrap_or_else(|| self.empty_session.clone());
90
+
91
+ let new_renderer = ctx
92
+ .props()
93
+ .workspace
94
+ .active_renderer()
95
+ .unwrap_or_else(|| self.empty_renderer.clone());
96
+
97
+ self.retarget_active(ctx, new_session, new_renderer);
98
+ let mut nudges = Vec::new();
99
+ for pid in prev.iter().chain(std::iter::once(&id)) {
100
+ if let Some(panel) = ctx.props().workspace.panel(pid)
101
+ && panel.renderer.is_plugin_activated().unwrap_or(false)
102
+ {
103
+ nudges.push(activation_render(
104
+ panel.session.clone(),
105
+ panel.renderer.clone(),
106
+ ));
107
+ }
108
+ }
109
+
110
+ let run = async move {
111
+ futures::future::join_all(nudges)
112
+ .await
113
+ .into_iter()
114
+ .collect::<ApiResult<Vec<_>>>()?;
115
+ Ok(())
116
+ };
117
+
118
+ match completion {
119
+ Some(completion) => completion.resolve_after(run),
120
+ None => spawn_owned("set-active-panel", run),
121
+ }
122
+
123
+ true
124
+ }
125
+ }
126
+
127
+ /// Whole-element `restore`'s single commit: the `Workspace` already holds
128
+ /// the final panel set (models inserted, olds ejected) and the staged
129
+ /// layout. Re-subscribe the per-panel title wiring (the set changed
130
+ /// wholesale), activate the restored panel, and re-render — `MainPanel`'s
131
+ /// `rendered` pass applies the staged layout and mounts every cell in
132
+ /// this one commit.
133
+ pub(super) fn on_commit_workspace_restore(&mut self, ctx: &Context<Self>, id: String) -> bool {
134
+ self._title_subscriptions = subscribe_panel_titles(ctx);
135
+ self.on_set_active_panel(ctx, id, None);
136
+ true
137
+ }
138
+
139
+ pub(super) fn on_close_panel(
140
+ &mut self,
141
+ ctx: &Context<Self>,
142
+ id: String,
143
+ completion: Option<Completion>,
144
+ ) -> bool {
145
+ let id = PanelId::from(id);
146
+
147
+ let was_active = ctx.props().workspace.active_id().as_ref() == Some(&id);
148
+ let removed = ctx.props().workspace.remove_panel(&id);
149
+ if was_active {
150
+ if let Some(next) = ctx.props().workspace.panel_ids().first().cloned() {
151
+ ctx.props().workspace.set_active(next);
152
+ }
153
+
154
+ let new_session = ctx
155
+ .props()
156
+ .workspace
157
+ .active_session()
158
+ .unwrap_or_else(|| self.empty_session.clone());
159
+ let new_renderer = ctx
160
+ .props()
161
+ .workspace
162
+ .active_renderer()
163
+ .unwrap_or_else(|| self.empty_renderer.clone());
164
+ self.retarget_active(ctx, new_session, new_renderer);
165
+ }
166
+
167
+ let eject = removed.map(eject_panel);
168
+ match (eject, completion) {
169
+ (Some(eject), Some(completion)) => completion.resolve_after(eject),
170
+ (Some(eject), None) => spawn_owned("close-panel", eject),
171
+ (None, Some(completion)) => completion.resolve_after(async { Ok(()) }),
172
+ (None, None) => {},
173
+ }
174
+
175
+ apply_global_filters(&ctx.props().workspace);
176
+ self._title_subscriptions = subscribe_panel_titles(ctx);
177
+ true
178
+ }
179
+
180
+ pub(super) fn on_duplicate_panel(&mut self, ctx: &Context<Self>, id: String) -> bool {
181
+ if let Some(panel) = ctx.props().workspace.panel(&PanelId::from(id)) {
182
+ let elem = ctx.props().elem.clone();
183
+ let presentation = ctx.props().presentation.clone();
184
+ let workspace = ctx.props().workspace.clone();
185
+ let notify = ctx.link().callback(|_: ()| LayoutChanged);
186
+ let activate = ctx.link().callback(|id| SetActivePanel(id, None));
187
+ ApiFuture::spawn(async move {
188
+ let config = panel
189
+ .renderer
190
+ .clone()
191
+ .with_lock(async {
192
+ get_viewer_config(&panel.session, &panel.renderer, &presentation).await
193
+ })
194
+ .await?;
195
+
196
+ let update = ViewerConfigUpdate::decode(&config.encode()?)?;
197
+ let client = panel.session.get_client();
198
+ let new_id = create_panel(
199
+ &elem,
200
+ &presentation,
201
+ &workspace,
202
+ &notify,
203
+ None,
204
+ update,
205
+ client,
206
+ )
207
+ .await?;
208
+
209
+ activate.emit(new_id.to_string());
210
+ Ok(())
211
+ });
212
+ }
213
+
214
+ false
215
+ }
216
+
217
+ pub(super) fn on_new_panel(&mut self, ctx: &Context<Self>, id: String) -> bool {
218
+ if let Some(panel) = ctx.props().workspace.panel(&PanelId::from(id)) {
219
+ let table_name = panel.session.get_table().map(|t| t.get_name().to_owned());
220
+ let elem = ctx.props().elem.clone();
221
+ let presentation = ctx.props().presentation.clone();
222
+ let workspace = ctx.props().workspace.clone();
223
+ let notify = ctx.link().callback(|_: ()| LayoutChanged);
224
+ let activate = ctx.link().callback(|id| SetActivePanel(id, None));
225
+ ApiFuture::spawn(async move {
226
+ let update = ViewerConfigUpdate {
227
+ table: table_name.map(TableUpdate::Update).unwrap_or_default(),
228
+ ..Default::default()
229
+ };
230
+
231
+ let client = panel.session.get_client();
232
+ let new_id = create_panel(
233
+ &elem,
234
+ &presentation,
235
+ &workspace,
236
+ &notify,
237
+ None,
238
+ update,
239
+ client,
240
+ )
241
+ .await?;
242
+
243
+ activate.emit(new_id.to_string());
244
+ Ok(())
245
+ });
246
+ }
247
+ false
248
+ }
249
+
250
+ /// `NewPanelFrom` — the context menu's "New" sub-menu: a fresh
251
+ /// (default-config) panel bound to the named `Table` on the named
252
+ /// `Client`, resolved from the `Workspace` loaded-clients registry.
253
+ pub(super) fn on_new_panel_from(
254
+ &mut self,
255
+ ctx: &Context<Self>,
256
+ client_name: String,
257
+ table: String,
258
+ ) -> bool {
259
+ let Some(client) = ctx
260
+ .props()
261
+ .workspace
262
+ .clients()
263
+ .into_iter()
264
+ .find(|c| c.get_name() == client_name)
265
+ else {
266
+ tracing::warn!("No loaded `Client` named \"{client_name}\"");
267
+ return false;
268
+ };
269
+
270
+ let elem = ctx.props().elem.clone();
271
+ let presentation = ctx.props().presentation.clone();
272
+ let workspace = ctx.props().workspace.clone();
273
+ let notify = ctx.link().callback(|_: ()| LayoutChanged);
274
+ let activate = ctx.link().callback(|id| SetActivePanel(id, None));
275
+ ApiFuture::spawn(async move {
276
+ let update = ViewerConfigUpdate {
277
+ table: TableUpdate::Update(table),
278
+ ..Default::default()
279
+ };
280
+
281
+ let new_id = create_panel(
282
+ &elem,
283
+ &presentation,
284
+ &workspace,
285
+ &notify,
286
+ None,
287
+ update,
288
+ Some(client),
289
+ )
290
+ .await?;
291
+
292
+ activate.emit(new_id.to_string());
293
+ Ok(())
294
+ });
295
+
296
+ false
297
+ }
298
+
299
+ /// Re-point the root's per-active engine wiring + snapshots from the
300
+ /// current active panel to a new one: clear the old panel's direct
301
+ /// callbacks, set up the new panel's callbacks + subscriptions, refresh
302
+ /// the snapshots, and reset the in-flight render counter (which tracked
303
+ /// the old panel).
304
+ fn retarget_active(&mut self, ctx: &Context<Self>, session: Session, renderer: Renderer) {
305
+ clear_active_callbacks(&self.active_session, &self.active_renderer);
306
+ inject_active_callbacks(ctx, &session, &renderer);
307
+ self._active_subscriptions = create_active_subscriptions(ctx, &session, &renderer);
308
+ self.session_props = session.to_props();
309
+ self.renderer_props = renderer.to_props(None);
310
+ self.update_count = session.in_flight_config_runs();
311
+ self.active_session = session;
312
+ self.active_renderer = renderer;
313
+ let presentation = ctx.props().presentation.clone();
314
+ let workspace = ctx.props().workspace.clone();
315
+ ApiFuture::spawn(async move {
316
+ let default_theme = presentation.get_default_theme_name().await;
317
+ if let Some(renderer) = workspace.active_renderer()
318
+ && let Some(theme) = renderer.theme().or(default_theme)
319
+ {
320
+ presentation.set_theme_name(Some(&theme)).await?;
321
+ }
322
+
323
+ Ok(())
324
+ });
325
+ }
326
+ }