@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,434 @@
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
+ //! Settings-sidebar handlers: the open/close toggle (with its presize
14
+ //! choreography), the deferred divider's latest-wins presize pump
15
+ //! (`PRESIZE_EVERYWHERE_PLAN.md` P1/P2), and the column-settings drawer. The
16
+ //! presize sweeps themselves live in [`crate::tasks`] (`presize_panels`).
17
+
18
+ use futures::channel::oneshot::{Sender, channel};
19
+ use perspective_js::utils::*;
20
+ use wasm_bindgen::prelude::*;
21
+ use yew::prelude::*;
22
+
23
+ use super::PerspectiveViewer;
24
+ use super::msg::PerspectiveViewerMsg::*;
25
+ use crate::components::font_loader::FontLoaderStatus;
26
+ use crate::components::settings_panel::SelectedTab;
27
+ use crate::config::*;
28
+ use crate::presentation::{ColumnLocator, ColumnSettingsTab};
29
+ use crate::tasks::*;
30
+
31
+ /// The settings sidebar's geometry state, folded into one field on
32
+ /// [`PerspectiveViewer`]: the pane/drawer width overrides, the divider
33
+ /// presize pump, and the open-state deltas cache.
34
+ #[derive(Default)]
35
+ pub(super) struct SettingsGeometry {
36
+ /// User-dragged settings-pane width (the deferred `SplitPanel`'s
37
+ /// controlled `size`); `None` until first dragged / after a reset.
38
+ pub pane_width_override: Option<i32>,
39
+
40
+ /// The selected settings-panel tab.
41
+ pub selected_tab: SelectedTab,
42
+
43
+ /// High-water-mark auto width reported by the settings panel.
44
+ pub auto_width: f64,
45
+
46
+ /// Latest-wins presize pump for the (deferred) settings divider: the
47
+ /// newest proposed pane width not yet presized, and whether a pump
48
+ /// iteration is in flight. See `SettingsDividerMove`.
49
+ pub divider_target: Option<i32>,
50
+ pub divider_pumping: bool,
51
+
52
+ /// Open-state geometry deltas `(layout_area.w − mpc.w, main_column.h −
53
+ /// mpc.h)`, cached at settings *close* time — used to presize panels to
54
+ /// their shrunk boxes BEFORE the pane mounts on the next *open* (P2).
55
+ /// `None` until the first close (the first-ever open is reactive).
56
+ pub open_deltas: Option<(f64, f64)>,
57
+
58
+ /// User-dragged column-settings drawer width override.
59
+ pub column_settings_width_override: Option<i32>,
60
+
61
+ /// High-water-mark auto width reported by the column-settings drawer -
62
+ /// the trap-door shared across its Style/Attributes/Window tabs, lifted
63
+ /// here (like `auto_width`) so it survives drawer re-opens and clears
64
+ /// on divider reset.
65
+ pub column_settings_auto_width: f64,
66
+
67
+ /// Whether the column-settings drawer is PINNED - laid out as a static
68
+ /// flex sibling between the main panel and the settings panel - rather
69
+ /// than FLOATING over the main panel (the default absolute overlay).
70
+ /// Session UI state, not part of the saved config.
71
+ pub column_settings_pinned: bool,
72
+ }
73
+
74
+ impl PerspectiveViewer {
75
+ pub(super) fn on_toggle_settings_init(
76
+ &mut self,
77
+ ctx: &Context<Self>,
78
+ update: Option<SettingsUpdate>,
79
+ resolve: Option<Sender<ApiResult<JsValue>>>,
80
+ ) -> bool {
81
+ match (update, resolve) {
82
+ (Some(SettingsUpdate::Missing), None) => false,
83
+ (Some(SettingsUpdate::Missing), Some(resolve)) => {
84
+ resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
85
+ false
86
+ },
87
+ (Some(SettingsUpdate::SetDefault), resolve) => {
88
+ self.init_toggle_settings_task(ctx, Some(false), resolve);
89
+ false
90
+ },
91
+ (Some(SettingsUpdate::Update(force)), resolve) => {
92
+ self.init_toggle_settings_task(ctx, Some(force), resolve);
93
+ false
94
+ },
95
+ (None, resolve) => {
96
+ self.init_toggle_settings_task(ctx, None, resolve);
97
+ false
98
+ },
99
+ }
100
+ }
101
+
102
+ pub(super) fn on_toggle_settings_complete(
103
+ &mut self,
104
+ ctx: &Context<Self>,
105
+ update: SettingsUpdate,
106
+ resolve: Sender<()>,
107
+ ) -> bool {
108
+ match update {
109
+ SettingsUpdate::SetDefault if self.settings_open => {
110
+ ctx.props().presentation.set_open_column_settings(None);
111
+ self.settings_open = false;
112
+ self.on_rendered.push(resolve);
113
+ true
114
+ },
115
+ SettingsUpdate::Update(force) if force != self.settings_open => {
116
+ ctx.props().presentation.set_open_column_settings(None);
117
+ self.settings_open = force;
118
+ self.on_rendered.push(resolve);
119
+ true
120
+ },
121
+ _ if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) => {
122
+ if let Err(e) = resolve.send(()) {
123
+ tracing::error!("toggle settings failed {:?}", e);
124
+ }
125
+
126
+ false
127
+ },
128
+ _ => {
129
+ ctx.props().presentation.set_open_column_settings(None);
130
+ self.on_rendered.push(resolve);
131
+ true
132
+ },
133
+ }
134
+ }
135
+
136
+ /// Toggle the settings, or force the settings panel either open (true) or
137
+ /// closed (false) explicitly. In order to reduce apparent
138
+ /// screen-shear, `toggle_settings()` uses a somewhat complex render
139
+ /// order: it first resize the plugin's `<div>` without moving it,
140
+ /// using `overflow: hidden` to hide the extra draw area; then,
141
+ /// after the _async_ drawing of the plugin is complete, it will send a
142
+ /// message to complete the toggle action and re-render the element with
143
+ /// the settings removed.
144
+ ///
145
+ /// # Arguments
146
+ /// * `force` - Whether to explicitly set the settings panel state to
147
+ /// Open/Close (`Some(true)`/`Some(false)`), or to just toggle the current
148
+ /// state (`None`).
149
+ fn init_toggle_settings_task(
150
+ &mut self,
151
+ ctx: &Context<Self>,
152
+ force: Option<bool>,
153
+ sender: Option<Sender<ApiResult<JsValue>>>,
154
+ ) {
155
+ let is_open = ctx.props().presentation.is_settings_open();
156
+ ctx.props().presentation.set_settings_before_open(!is_open);
157
+ match force {
158
+ Some(force) if is_open == force => {
159
+ if let Some(sender) = sender {
160
+ sender.send(Ok(JsValue::UNDEFINED)).unwrap();
161
+ }
162
+ },
163
+ Some(_) | None => {
164
+ let force = !is_open;
165
+ let callback = ctx.link().callback(move |resolve| {
166
+ let update = SettingsUpdate::Update(force);
167
+ ToggleSettingsComplete(update, resolve)
168
+ });
169
+
170
+ // P2: at CLOSE time the open-state geometry is measurable —
171
+ // cache the deltas the next OPEN needs to presize with.
172
+ if is_open {
173
+ self.settings_geometry.open_deltas =
174
+ measure_settings_open_deltas(&ctx.props().elem);
175
+ }
176
+
177
+ let open_deltas = self.settings_geometry.open_deltas;
178
+ let workspace = ctx.props().workspace.clone();
179
+ let presentation = ctx.props().presentation.clone();
180
+ let elem = ctx.props().elem.clone();
181
+
182
+ ApiFuture::spawn(async move {
183
+ // Resize every visible plugin (not just the active one) to its
184
+ // new cell as the settings pane toggles. The pane is the outer
185
+ // SplitPanel, which emits no `before-resize`, so this is driven
186
+ // explicitly here. CLOSING grows the cells, so pre-size each
187
+ // plugin to its grown box BEFORE collapsing the pane; OPENING
188
+ // shrinks them, so pre-size to the shrunk box (from the deltas
189
+ // cached at the last close; the first-ever open has none and
190
+ // stays reactive) BEFORE the pane mounts — either way, one
191
+ // clean resize with the reactive pass after as the exactness
192
+ // finalizer.
193
+ let result: ApiResult<JsValue> = async {
194
+ if is_open {
195
+ let presents = presize_visible_panels_grown(&workspace, &elem).await;
196
+ let (notify, rendered) = channel::<()>();
197
+ callback.emit(notify);
198
+ presentation.set_settings_open(false);
199
+ rendered.await?;
200
+ // `notify` fires in `rendered()` (same task as the
201
+ // Yew DOM patch) and this future resumes as one of
202
+ // its microtasks — so the staged reveal below and
203
+ // the pane's geometry change reach the screen in a
204
+ // single paint.
205
+ presents.reveal();
206
+ // I6: the exactness-finalizer resize is part of
207
+ // what this toggle caused — await it here rather
208
+ // than leaving it to the ResizeObserver (whose
209
+ // continuous pass remains as the reactive
210
+ // backstop).
211
+ resize_visible_panels(&workspace).await;
212
+ } else {
213
+ let presents = if let Some((delta_w, delta_h)) = open_deltas {
214
+ presize_visible_panels_open(&workspace, &elem, delta_w, delta_h)
215
+ .await
216
+ } else {
217
+ StagedPresents::default()
218
+ };
219
+
220
+ let (notify, rendered) = channel::<()>();
221
+ callback.emit(notify);
222
+ presentation.set_settings_open(true);
223
+ rendered.await?;
224
+ presents.reveal();
225
+ resize_visible_panels(&workspace).await;
226
+ }
227
+ Ok(JsValue::UNDEFINED)
228
+ }
229
+ .await;
230
+
231
+ if let Some(sender) = sender {
232
+ let msg = result.ignore_view_delete();
233
+ sender
234
+ .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
235
+ .into_apierror()?;
236
+ };
237
+
238
+ Ok(JsValue::undefined())
239
+ });
240
+ },
241
+ };
242
+ }
243
+
244
+ pub(super) fn on_settings_panel_size_update(&mut self, x: Option<i32>) -> bool {
245
+ match x {
246
+ Some(x) => {
247
+ self.settings_geometry.pane_width_override = Some(x);
248
+ false
249
+ },
250
+ None => {
251
+ self.settings_geometry.pane_width_override = None;
252
+ self.settings_geometry.auto_width = 0.0;
253
+ self.on_settings_panel_dimensions_reset.emit(());
254
+ true
255
+ },
256
+ }
257
+ }
258
+
259
+ pub(super) fn on_settings_divider_move(
260
+ &mut self,
261
+ ctx: &Context<Self>,
262
+ pane_width: i32,
263
+ ) -> bool {
264
+ // Latest-wins: overwrite the pending target; start the pump if
265
+ // idle. Intermediate targets that arrive while a presize is in
266
+ // flight are dropped (mirrors `PresizeQueue`'s single queued
267
+ // slot) — the pane tracks the pointer at content-render rate.
268
+ self.settings_geometry.divider_target = Some(pane_width);
269
+ if !self.settings_geometry.divider_pumping {
270
+ self.settings_geometry.divider_pumping = true;
271
+ ctx.link().send_message(SettingsDividerPump);
272
+ }
273
+
274
+ false
275
+ }
276
+
277
+ pub(super) fn on_settings_divider_pump(&mut self, ctx: &Context<Self>) -> bool {
278
+ if let Some(pane_width) = self.settings_geometry.divider_target.take() {
279
+ let workspace = ctx.props().workspace.clone();
280
+ let elem = ctx.props().elem.clone();
281
+ let link = ctx.link().clone();
282
+ ApiFuture::spawn(async move {
283
+ let presents =
284
+ presize_visible_panels_pane_width(&workspace, &elem, pane_width as f64).await;
285
+ link.send_message(SettingsDividerCommit(pane_width));
286
+ // Same task as the commit's re-render (whether Yew drained it
287
+ // synchronously inside `send_message` or deferred it to a
288
+ // microtask): the staged reveal and the pane-width geometry
289
+ // land in one paint.
290
+ presents.reveal();
291
+ Ok(())
292
+ });
293
+ } else {
294
+ self.settings_geometry.divider_pumping = false;
295
+ }
296
+
297
+ false
298
+ }
299
+
300
+ pub(super) fn on_settings_divider_commit(
301
+ &mut self,
302
+ ctx: &Context<Self>,
303
+ pane_width: i32,
304
+ ) -> bool {
305
+ // Every visible panel has rendered at (approximately) its
306
+ // target box — NOW move the geometry: the re-render below
307
+ // applies this width to the deferred `SplitPanel`'s controlled
308
+ // `size`, in the same task as the presizes' inline clears.
309
+ self.settings_geometry.pane_width_override = Some(pane_width);
310
+ ctx.link().send_message(SettingsDividerPump);
311
+ true
312
+ }
313
+
314
+ pub(super) fn on_settings_divider_finish(&mut self, ctx: &Context<Self>) -> bool {
315
+ let workspace = ctx.props().workspace.clone();
316
+ ApiFuture::spawn(async move {
317
+ resize_visible_panels(&workspace).await;
318
+ Ok(())
319
+ });
320
+
321
+ false
322
+ }
323
+
324
+ pub(super) fn on_settings_panel_tab_changed(&mut self, tab: SelectedTab) -> bool {
325
+ let changed = tab != self.settings_geometry.selected_tab;
326
+ self.settings_geometry.selected_tab = tab;
327
+ changed
328
+ }
329
+
330
+ pub(super) fn on_settings_panel_auto_width(&mut self, w: f64) -> bool {
331
+ if w > self.settings_geometry.auto_width {
332
+ self.settings_geometry.auto_width = w;
333
+ true
334
+ } else {
335
+ false
336
+ }
337
+ }
338
+
339
+ pub(super) fn on_open_column_settings(
340
+ &mut self,
341
+ ctx: &Context<Self>,
342
+ locator: Option<ColumnLocator>,
343
+ sender: Option<Sender<()>>,
344
+ toggle: bool,
345
+ ) -> bool {
346
+ let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
347
+ if locator == open_column_settings.locator {
348
+ if toggle {
349
+ ctx.props().presentation.set_open_column_settings(None);
350
+ }
351
+ } else {
352
+ open_column_settings.locator.clone_from(&locator);
353
+ open_column_settings.tab = if matches!(locator, Some(ColumnLocator::NewExpression)) {
354
+ Some(ColumnSettingsTab::Attributes)
355
+ } else {
356
+ locator.as_ref().and_then(|x| {
357
+ x.name().map(|x| {
358
+ if self.session_props.is_column_active(x) {
359
+ ColumnSettingsTab::Style
360
+ } else {
361
+ ColumnSettingsTab::Attributes
362
+ }
363
+ })
364
+ })
365
+ };
366
+
367
+ ctx.props()
368
+ .presentation
369
+ .set_open_column_settings(Some(open_column_settings));
370
+
371
+ if locator.is_some() {
372
+ self.settings_geometry.selected_tab = SelectedTab::Query;
373
+ }
374
+ }
375
+
376
+ if let Some(sender) = sender {
377
+ // I6: resolve on the render commit that applies this change (the
378
+ // shared `on_rendered` queue, drained in `rendered()` once fonts
379
+ // settle), not at message-handling time.
380
+ self.on_rendered.push(sender);
381
+ }
382
+
383
+ true
384
+ }
385
+
386
+ pub(super) fn on_column_settings_panel_size_update(&mut self, x: Option<i32>) -> bool {
387
+ self.settings_geometry.column_settings_width_override = x;
388
+ if x.is_none() {
389
+ self.settings_geometry.column_settings_auto_width = 0.0;
390
+ true
391
+ } else {
392
+ false
393
+ }
394
+ }
395
+
396
+ pub(super) fn on_column_settings_panel_auto_width(&mut self, w: f64) -> bool {
397
+ if w > self.settings_geometry.column_settings_auto_width {
398
+ self.settings_geometry.column_settings_auto_width = w;
399
+ true
400
+ } else {
401
+ false
402
+ }
403
+ }
404
+
405
+ pub(super) fn on_toggle_column_settings_pin(&mut self) -> bool {
406
+ self.settings_geometry.column_settings_pinned =
407
+ !self.settings_geometry.column_settings_pinned;
408
+ true
409
+ }
410
+
411
+ pub(super) fn on_column_settings_tab_changed(
412
+ &mut self,
413
+ ctx: &Context<Self>,
414
+ tab: ColumnSettingsTab,
415
+ ) -> bool {
416
+ let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
417
+ open_column_settings.tab.clone_from(&Some(tab));
418
+ ctx.props()
419
+ .presentation
420
+ .set_open_column_settings(Some(open_column_settings));
421
+ true
422
+ }
423
+
424
+ /// Toggling the debug tab re-renders the settings panel only — the
425
+ /// `DebugPanel` populates itself (`get_viewer_config` on mount +
426
+ /// change subscriptions), so no plugin dispatch is owed. The old
427
+ /// `just_render` here relied on the pre-amendment unconditional
428
+ /// `Unchanged → update` arm and repainted the plugin as a side effect
429
+ /// (`PLUGIN_DRAW_INVARIANT_PLAN.md` amendment, migration 2).
430
+ pub(super) fn on_toggle_debug(&mut self, _ctx: &Context<Self>) -> bool {
431
+ self.debug_open = !self.debug_open;
432
+ true
433
+ }
434
+ }
@@ -0,0 +1,134 @@
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
+ //! Value-semantic snapshot plumbing: the mechanical `Update*` handlers that
14
+ //! copy fresh engine-state snapshots (dispatched by the [`super::wiring`]
15
+ //! subscriptions/callbacks) into the root's props fields, re-rendering only
16
+ //! on an actual change.
17
+
18
+ use yew::prelude::*;
19
+
20
+ use super::PerspectiveViewer;
21
+ use crate::presentation::{DragDropProps, OpenColumnSettings, PresentationProps};
22
+ use crate::renderer::RendererProps;
23
+ use crate::session::{SessionProps, TableLoadState, ViewStats};
24
+
25
+ impl PerspectiveViewer {
26
+ pub(super) fn on_update_session(&mut self, props: SessionProps) -> bool {
27
+ let changed = props != self.session_props;
28
+ self.session_props = props;
29
+ changed
30
+ }
31
+
32
+ pub(super) fn on_update_session_stats(
33
+ &mut self,
34
+ stats: Option<ViewStats>,
35
+ has_table: Option<TableLoadState>,
36
+ ) -> bool {
37
+ let changed =
38
+ stats != self.session_props.stats || has_table != self.session_props.has_table;
39
+ self.session_props.stats = stats;
40
+ self.session_props.has_table = has_table;
41
+ changed
42
+ }
43
+
44
+ pub(super) fn on_update_renderer(&mut self, props: RendererProps) -> bool {
45
+ let changed = props != self.renderer_props;
46
+ self.renderer_props = props;
47
+ changed
48
+ }
49
+
50
+ pub(super) fn on_update_presentation(
51
+ &mut self,
52
+ ctx: &Context<Self>,
53
+ props: PresentationProps,
54
+ ) -> bool {
55
+ // Default-theme fan-out: when the registry default (first available
56
+ // theme) changes — the async theme discovery resolving at boot, or a
57
+ // `resetThemes` — push the new default into every panel's renderer
58
+ // cache (locked draws stamp the effective theme from it), and
59
+ // restyle the panels whose captured `--psp-*` CSS is STALE against
60
+ // the new effective value (`Renderer::needs_restyle` — the plugin's
61
+ // captured theme is the baseline; plugins only re-read CSS at
62
+ // `restyle()`/first-draw, so a plain redraw would not repaint a
63
+ // panel that first drew before discovery resolved, while a panel
64
+ // that captured post-discovery — or owes its first paint — restyles
65
+ // nothing). The outer default-diff scopes the scan; the per-panel
66
+ // gate is state, never call history or DOM state.
67
+ let old_default = self.presentation_props.available_themes.first().cloned();
68
+ let new_default = props.available_themes.first().cloned();
69
+ if old_default != new_default {
70
+ for panel in ctx
71
+ .props()
72
+ .workspace
73
+ .panel_ids()
74
+ .into_iter()
75
+ .filter_map(|id| ctx.props().workspace.panel(&id))
76
+ {
77
+ panel.renderer.set_default_theme(new_default.clone());
78
+ if panel.renderer.needs_restyle() {
79
+ let renderer = panel.renderer.clone();
80
+ crate::utils::spawn_owned("default-theme-restyle", async move {
81
+ renderer.restyle_all().await?;
82
+ Ok(())
83
+ });
84
+ }
85
+ }
86
+ }
87
+
88
+ let changed = props != self.presentation_props;
89
+ self.presentation_props = props;
90
+ changed
91
+ }
92
+
93
+ pub(super) fn on_update_settings_open(&mut self, open: bool) -> bool {
94
+ let changed = open != self.presentation_props.is_settings_open;
95
+ self.presentation_props.is_settings_open = open;
96
+ changed
97
+ }
98
+
99
+ pub(super) fn on_update_is_workspace(&mut self, is_workspace: bool) -> bool {
100
+ let changed = is_workspace != self.presentation_props.is_workspace;
101
+ self.presentation_props.is_workspace = is_workspace;
102
+ changed
103
+ }
104
+
105
+ pub(super) fn on_update_column_settings(&mut self, ocs: OpenColumnSettings) -> bool {
106
+ let changed = ocs != self.presentation_props.open_column_settings;
107
+ self.presentation_props.open_column_settings = ocs;
108
+ changed
109
+ }
110
+
111
+ pub(super) fn on_update_dragdrop(&mut self, props: DragDropProps) -> bool {
112
+ let changed = props != self.dragdrop_props;
113
+ self.dragdrop_props = props;
114
+ changed
115
+ }
116
+
117
+ pub(super) fn on_update_global_filters(&mut self, ctx: &Context<Self>) -> bool {
118
+ let filters = ctx.props().workspace.global_filters();
119
+ let changed = filters != self.global_filters;
120
+ self.global_filters = filters;
121
+ changed
122
+ }
123
+
124
+ /// LEVEL-triggered spinner count: ASSIGN the absolute in-flight
125
+ /// config-run count from the session's RAII accounting — any missed or
126
+ /// reordered notification is corrected by the next one, unlike the
127
+ /// edge-counted increment/decrement pair this replaces (see
128
+ /// `UPDATE_COUNT_REGRESSION_PLAN.md`).
129
+ pub(super) fn on_update_in_flight(&mut self, count: u32) -> bool {
130
+ let changed = count != self.update_count;
131
+ self.update_count = count;
132
+ changed
133
+ }
134
+ }