@perspective-dev/viewer 4.5.2 → 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 (178) 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.d.ts +9 -0
  18. package/dist/esm/perspective-viewer.inline.js +20 -1
  19. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  20. package/dist/esm/perspective-viewer.js +20 -1
  21. package/dist/esm/perspective-viewer.js.map +4 -4
  22. package/dist/esm/plugin.d.ts +71 -5
  23. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  24. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  25. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  26. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  27. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  28. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  29. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  30. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  32. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  33. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  34. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  35. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  36. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  37. package/dist/wasm/perspective-viewer.d.ts +211 -77
  38. package/dist/wasm/perspective-viewer.js +528 -180
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  41. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  42. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  43. package/package.json +3 -2
  44. package/src/css/_context_menu.css +14 -0
  45. package/src/css/column-settings-panel.css +1 -1
  46. package/src/css/containers/context-menu.css +101 -0
  47. package/src/css/panel-tab.css +193 -0
  48. package/src/css/plugin-selector.css +7 -10
  49. package/src/css/status-bar.css +75 -81
  50. package/src/css/viewer.css +180 -70
  51. package/src/rust/components/column_dropdown.rs +40 -30
  52. package/src/rust/components/column_selector/active_column.rs +7 -18
  53. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  54. package/src/rust/components/column_selector/config_selector.rs +33 -81
  55. package/src/rust/components/column_selector/filter_column.rs +5 -15
  56. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  57. package/src/rust/components/column_selector/sort_column.rs +3 -6
  58. package/src/rust/components/column_selector.rs +5 -11
  59. package/src/rust/components/containers/split_panel.rs +175 -62
  60. package/src/rust/components/context_menu.rs +130 -0
  61. package/src/rust/components/form/debug.rs +3 -0
  62. package/src/rust/components/global_filter_bar.rs +76 -0
  63. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  64. package/src/rust/components/main_panel/msg.rs +62 -0
  65. package/src/rust/components/main_panel/presize.rs +220 -0
  66. package/src/rust/components/main_panel/reconcile.rs +224 -0
  67. package/src/rust/components/main_panel/render.rs +385 -0
  68. package/src/rust/components/main_panel/update.rs +184 -0
  69. package/src/rust/components/main_panel.rs +311 -71
  70. package/src/rust/components/mod.rs +4 -0
  71. package/src/rust/components/panel_menu.rs +486 -0
  72. package/src/rust/components/panel_tab.rs +505 -0
  73. package/src/rust/components/plugin_tab.rs +1 -1
  74. package/src/rust/components/settings_panel.rs +13 -5
  75. package/src/rust/components/status_bar.rs +110 -178
  76. package/src/rust/components/status_indicator.rs +6 -9
  77. package/src/rust/components/style/mod.rs +5 -0
  78. package/src/rust/components/style/style_provider.rs +2 -0
  79. package/src/rust/components/viewer/filters.rs +133 -0
  80. package/src/rust/components/viewer/msg.rs +180 -0
  81. package/src/rust/components/viewer/panels.rs +326 -0
  82. package/src/rust/components/viewer/render.rs +243 -0
  83. package/src/rust/components/viewer/settings.rs +402 -0
  84. package/src/rust/components/viewer/snapshots.rs +134 -0
  85. package/src/rust/components/viewer/wiring.rs +505 -0
  86. package/src/rust/components/viewer.rs +243 -756
  87. package/src/rust/config/export_method.rs +1 -1
  88. package/src/rust/config/mod.rs +4 -0
  89. package/src/rust/config/options.rs +71 -0
  90. package/src/rust/config/viewer_config.rs +24 -2
  91. package/src/rust/config/workspace_config.rs +92 -0
  92. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  93. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  94. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  95. package/src/rust/custom_elements/viewer.rs +1015 -286
  96. package/src/rust/custom_events.rs +212 -61
  97. package/src/rust/js/mod.rs +2 -0
  98. package/src/rust/js/plugin.rs +297 -10
  99. package/src/rust/js/regular_layout.rs +258 -0
  100. package/src/rust/lib.rs +20 -1
  101. package/src/rust/presentation.rs +66 -33
  102. package/src/rust/queries/get_viewer_config.rs +29 -11
  103. package/src/rust/queries/plugin_column_styles.rs +2 -2
  104. package/src/rust/renderer/activate.rs +41 -14
  105. package/src/rust/renderer/dispatch.rs +436 -0
  106. package/src/rust/renderer/plugin_config.rs +495 -0
  107. package/src/rust/renderer/registry.rs +18 -6
  108. package/src/rust/renderer/render_timer.rs +29 -10
  109. package/src/rust/renderer.rs +350 -837
  110. package/src/rust/session/column_defaults_update.rs +26 -8
  111. package/src/rust/session/view_subscription.rs +30 -15
  112. package/src/rust/session.rs +753 -264
  113. package/src/rust/tasks/apply_global_filters.rs +89 -0
  114. package/src/rust/tasks/auto_pause.rs +171 -0
  115. package/src/rust/tasks/copy_export.rs +29 -19
  116. package/src/rust/tasks/create_panel.rs +243 -0
  117. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  118. package/src/rust/tasks/edit_expression.rs +4 -4
  119. package/src/rust/tasks/eject.rs +64 -23
  120. package/src/rust/tasks/mod.rs +28 -4
  121. package/src/rust/tasks/pipeline.rs +411 -0
  122. package/src/rust/tasks/presize_panels.rs +324 -0
  123. package/src/rust/tasks/reset_all.rs +22 -12
  124. package/src/rust/tasks/resize_observer.rs +124 -87
  125. package/src/rust/tasks/restore_and_render.rs +80 -86
  126. package/src/rust/tasks/restore_panel.rs +145 -0
  127. package/src/rust/tasks/send_column_config.rs +5 -2
  128. package/src/rust/tasks/send_plugin_config.rs +5 -2
  129. package/src/rust/tasks/set_edit_mode.rs +59 -0
  130. package/src/rust/tasks/sync_update_panels.rs +152 -0
  131. package/src/rust/tasks/table_lifecycle.rs +158 -0
  132. package/src/rust/tasks/update_theme.rs +56 -12
  133. package/src/rust/utils/completion.rs +56 -0
  134. package/src/rust/utils/custom_element.rs +5 -0
  135. package/src/rust/utils/debounce.rs +130 -7
  136. package/src/rust/utils/mod.rs +6 -0
  137. package/src/rust/utils/pending_effects.rs +62 -0
  138. package/src/rust/utils/spawn.rs +33 -0
  139. package/src/rust/utils/weak_scope.rs +0 -10
  140. package/src/rust/workspace.rs +846 -0
  141. package/src/svg/active-indicator.svg +3 -0
  142. package/src/themes/botanical.css +5 -2
  143. package/src/themes/defaults.css +54 -2
  144. package/src/themes/dracula.css +6 -3
  145. package/src/themes/gruvbox-dark.css +5 -2
  146. package/src/themes/gruvbox.css +5 -2
  147. package/src/themes/icons.css +4 -2
  148. package/src/themes/monokai.css +5 -2
  149. package/src/themes/phosphor.css +5 -2
  150. package/src/themes/pro-dark.css +5 -2
  151. package/src/themes/pro.css +2 -1
  152. package/src/themes/solarized-dark.css +5 -2
  153. package/src/themes/solarized.css +5 -2
  154. package/src/themes/vaporwave.css +5 -2
  155. package/src/ts/bootstrap.ts +8 -7
  156. package/src/ts/extensions.ts +30 -2
  157. package/src/ts/perspective-viewer.ts +10 -0
  158. package/src/ts/plugin.ts +72 -5
  159. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  160. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  161. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  162. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  163. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  164. package/src/ts/ts-rs/Layout.ts +9 -0
  165. package/src/ts/ts-rs/Orientation.ts +6 -0
  166. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  168. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  169. package/src/ts/ts-rs/TabLayout.ts +11 -0
  170. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  171. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  172. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  173. package/src/rust/tasks/intersection_observer.rs +0 -98
  174. package/src/rust/tasks/update_and_render.rs +0 -110
  175. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -0,0 +1,243 @@
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 `view()`: composition of the settings pane, the
14
+ //! column-settings drawer, and the multi-panel `MainPanel`.
15
+ //!
16
+ //! INVARIANT: the `SplitPanel` is ALWAYS rendered with `main_panel` as its
17
+ //! last (flex-fill) pane — toggling the settings sidebar only adds/removes the
18
+ //! leading pane, never reparents `main_panel` (a remount would tear down
19
+ //! `<regular-layout>` + the plugin `<slot>`s).
20
+
21
+ use yew::prelude::*;
22
+
23
+ use super::PerspectiveViewer;
24
+ use super::msg::PerspectiveViewerMsg::*;
25
+ use crate::components::column_settings_sidebar::ColumnSettingsPanel;
26
+ use crate::components::containers::split_panel::SplitPanel;
27
+ use crate::components::font_loader::FontLoader;
28
+ use crate::components::main_panel::MainPanel;
29
+ use crate::components::panel_menu::PanelCommand;
30
+ use crate::components::settings_panel::SettingsPanel;
31
+ use crate::components::style::StyleProvider;
32
+ use crate::queries::*;
33
+ use crate::session::TableLoadState;
34
+
35
+ impl PerspectiveViewer {
36
+ pub(super) fn render(&self, ctx: &Context<Self>) -> Html {
37
+ // The settings panel + status bar bind to the *active* panel's engines;
38
+ // `presentation` is element-level (shared).
39
+ let presentation = &ctx.props().presentation;
40
+ let renderer = &self.active_renderer;
41
+ let session = &self.active_session;
42
+
43
+ let is_settings_open = self.settings_open
44
+ && matches!(self.session_props.has_table, Some(TableLoadState::Loaded));
45
+
46
+ let mut class = classes!();
47
+ if !is_settings_open {
48
+ class.push("settings-closed");
49
+ }
50
+
51
+ if self.session_props.title.is_some() {
52
+ class.push("titled");
53
+ }
54
+
55
+ let on_open_expr_panel = ctx.link().callback(|c| OpenColumnSettings {
56
+ locator: c,
57
+ sender: None,
58
+ toggle: true,
59
+ });
60
+
61
+ let on_column_settings_panel_resize = ctx
62
+ .link()
63
+ .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
64
+
65
+ let on_close_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
66
+ let on_debug = ctx.link().callback(|_| ToggleDebug);
67
+ let selected_column = get_current_column_locator(
68
+ &self.presentation_props.open_column_settings,
69
+ renderer,
70
+ &self.session_props.config,
71
+ &self.session_props.metadata,
72
+ );
73
+
74
+ let selected_tab = self.presentation_props.open_column_settings.tab;
75
+ let plugin_name = self.renderer_props.plugin_name.clone();
76
+ let available_plugins = self.renderer_props.available_plugins.clone();
77
+ let has_table = self.session_props.has_table.clone();
78
+ let named_column_count = self.renderer_props.config.config_column_names.len();
79
+
80
+ let view_config = self.session_props.config.clone();
81
+ let drag_column = self.dragdrop_props.column.clone();
82
+ let metadata = self.session_props.metadata.clone();
83
+ let on_select_tab = ctx.link().callback(SettingsPanelTabChanged);
84
+ let on_auto_width = ctx.link().callback(SettingsPanelAutoWidth);
85
+ let settings_panel = if is_settings_open {
86
+ html! {
87
+ <SettingsPanel
88
+ on_close={on_close_settings}
89
+ on_resize={&self.on_resize}
90
+ on_select_column={on_open_expr_panel}
91
+ is_debug={self.debug_open}
92
+ {on_debug}
93
+ {plugin_name}
94
+ {available_plugins}
95
+ {has_table}
96
+ {named_column_count}
97
+ {view_config}
98
+ plugin_config={self.renderer_props.plugin_config.clone()}
99
+ {drag_column}
100
+ metadata={metadata.clone()}
101
+ open_column_settings={self.presentation_props.open_column_settings.clone()}
102
+ selected_theme={self.presentation_props.selected_theme.clone()}
103
+ selected_tab={self.settings_geometry.selected_tab}
104
+ auto_width={self.settings_geometry.auto_width}
105
+ on_dimensions_reset={&self.on_settings_panel_dimensions_reset}
106
+ {on_select_tab}
107
+ {on_auto_width}
108
+ {presentation}
109
+ {renderer}
110
+ {session}
111
+ />
112
+ }
113
+ } else {
114
+ html! { <></> }
115
+ };
116
+
117
+ let on_settings = ctx.link().callback(|()| ToggleSettingsInit(None, None));
118
+ let on_select_tab = ctx.link().callback(ColumnSettingsTabChanged);
119
+ let column_settings_panel = html! {
120
+ if let Some(selected_column) = selected_column {
121
+ <SplitPanel
122
+ id="modal_panel"
123
+ reverse=true
124
+ initial_size={self.settings_geometry.column_settings_width_override}
125
+ on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
126
+ on_resize={on_column_settings_panel_resize}
127
+ >
128
+ <ColumnSettingsPanel
129
+ {selected_column}
130
+ {selected_tab}
131
+ on_close={self.on_close_column_settings.clone()}
132
+ width_override={self.settings_geometry.column_settings_width_override}
133
+ {on_select_tab}
134
+ plugin_name={self.renderer_props.plugin_name.clone()}
135
+ {metadata}
136
+ view_config={self.session_props.config.clone()}
137
+ column_stats={self.session_props.column_stats.clone()}
138
+ selected_theme={self.presentation_props.selected_theme.clone()}
139
+ {presentation}
140
+ {renderer}
141
+ {session}
142
+ />
143
+ <></>
144
+ </SplitPanel>
145
+ }
146
+ };
147
+
148
+ let on_reset = ctx.link().callback(|all| ResetPanel(None, all, None));
149
+ let main_panel = html! {
150
+ <MainPanel
151
+ {on_settings}
152
+ {on_reset}
153
+ on_activate_panel={ctx.link().callback(|id| SetActivePanel(id, None))}
154
+ on_close_panel={ctx.link().callback(|id| ClosePanel(id, None))}
155
+ on_panel_command={ctx.link().batch_callback(|(id, cmd): (String, PanelCommand)| {
156
+ match cmd {
157
+ PanelCommand::New => vec![NewPanel(id)],
158
+ PanelCommand::NewFrom { client, table } => {
159
+ vec![NewPanelFrom { client, table }]
160
+ },
161
+ PanelCommand::Duplicate => vec![DuplicatePanel(id)],
162
+ PanelCommand::Reset => vec![ResetPanel(Some(id), false, None)],
163
+ PanelCommand::ToggleMaster => vec![ToggleMaster(id)],
164
+ PanelCommand::Close => vec![ClosePanel(id, None)],
165
+ PanelCommand::Maximize | PanelCommand::Restore => vec![],
166
+ }
167
+ })}
168
+ session_props={self.session_props.clone()}
169
+ renderer_props={self.renderer_props.clone()}
170
+ presentation_props={self.presentation_props.clone()}
171
+ {is_settings_open}
172
+ update_count={self.update_count}
173
+ panel_ids={ctx.props().workspace.panel_ids()}
174
+ panel_titles={ctx.props()
175
+ .workspace
176
+ .panel_ids()
177
+ .iter()
178
+ .map(|id| {
179
+ let title = ctx
180
+ .props()
181
+ .workspace
182
+ .panel(id)
183
+ .and_then(|p| p.session.get_title())
184
+ .filter(|t| !t.is_empty());
185
+ (id.as_str().to_owned(), title)
186
+ })
187
+ .collect::<Vec<_>>()}
188
+ panel_themes={ctx.props()
189
+ .workspace
190
+ .panel_ids()
191
+ .iter()
192
+ .map(|id| {
193
+ (
194
+ id.as_str().to_owned(),
195
+ ctx.props().workspace.panel(id).and_then(|p| p.renderer.theme()),
196
+ )
197
+ })
198
+ .collect::<Vec<_>>()}
199
+ panel_masters={ctx.props().workspace.masters()}
200
+ workspace={ctx.props().workspace.clone()}
201
+ global_filters={self.global_filters.clone()}
202
+ on_remove_global_filter={ctx.link().callback(RemoveGlobalFilter)}
203
+ on_clear_global_filters={ctx.link().callback(|_| ClearGlobalFilters)}
204
+ {presentation}
205
+ {renderer}
206
+ {session}
207
+ />
208
+ };
209
+
210
+ let is_single_panel = if ctx.props().workspace.placed_count() == 1 {
211
+ "only-child"
212
+ } else {
213
+ ""
214
+ };
215
+
216
+ html! {
217
+ <StyleProvider root={ctx.props().elem.clone()}>
218
+ <div id="component_container" class={is_single_panel}>
219
+ <div id="layout_area">
220
+ <SplitPanel
221
+ id="app_panel"
222
+ reverse=true
223
+ skip_empty=true
224
+ deferred=true
225
+ size={self.settings_geometry.pane_width_override}
226
+ initial_size={self.settings_geometry.pane_width_override}
227
+ on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
228
+ on_resize={ctx.link().callback(|(x, _): (i32, i32)| SettingsDividerMove(x))}
229
+ on_resize_finished={ctx.link().callback(|_| SettingsDividerFinish)}
230
+ >
231
+ { settings_panel }
232
+ <div id="main_column_container">
233
+ { main_panel }
234
+ { column_settings_panel }
235
+ </div>
236
+ </SplitPanel>
237
+ </div>
238
+ </div>
239
+ <FontLoader ..self.fonts.clone() />
240
+ </StyleProvider>
241
+ }
242
+ }
243
+ }
@@ -0,0 +1,402 @@
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
+
62
+ impl PerspectiveViewer {
63
+ pub(super) fn on_toggle_settings_init(
64
+ &mut self,
65
+ ctx: &Context<Self>,
66
+ update: Option<SettingsUpdate>,
67
+ resolve: Option<Sender<ApiResult<JsValue>>>,
68
+ ) -> bool {
69
+ match (update, resolve) {
70
+ (Some(SettingsUpdate::Missing), None) => false,
71
+ (Some(SettingsUpdate::Missing), Some(resolve)) => {
72
+ resolve.send(Ok(JsValue::UNDEFINED)).unwrap();
73
+ false
74
+ },
75
+ (Some(SettingsUpdate::SetDefault), resolve) => {
76
+ self.init_toggle_settings_task(ctx, Some(false), resolve);
77
+ false
78
+ },
79
+ (Some(SettingsUpdate::Update(force)), resolve) => {
80
+ self.init_toggle_settings_task(ctx, Some(force), resolve);
81
+ false
82
+ },
83
+ (None, resolve) => {
84
+ self.init_toggle_settings_task(ctx, None, resolve);
85
+ false
86
+ },
87
+ }
88
+ }
89
+
90
+ pub(super) fn on_toggle_settings_complete(
91
+ &mut self,
92
+ ctx: &Context<Self>,
93
+ update: SettingsUpdate,
94
+ resolve: Sender<()>,
95
+ ) -> bool {
96
+ match update {
97
+ SettingsUpdate::SetDefault if self.settings_open => {
98
+ ctx.props().presentation.set_open_column_settings(None);
99
+ self.settings_open = false;
100
+ self.on_rendered.push(resolve);
101
+ true
102
+ },
103
+ SettingsUpdate::Update(force) if force != self.settings_open => {
104
+ ctx.props().presentation.set_open_column_settings(None);
105
+ self.settings_open = force;
106
+ self.on_rendered.push(resolve);
107
+ true
108
+ },
109
+ _ if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) => {
110
+ if let Err(e) = resolve.send(()) {
111
+ tracing::error!("toggle settings failed {:?}", e);
112
+ }
113
+
114
+ false
115
+ },
116
+ _ => {
117
+ ctx.props().presentation.set_open_column_settings(None);
118
+ self.on_rendered.push(resolve);
119
+ true
120
+ },
121
+ }
122
+ }
123
+
124
+ /// Toggle the settings, or force the settings panel either open (true) or
125
+ /// closed (false) explicitly. In order to reduce apparent
126
+ /// screen-shear, `toggle_settings()` uses a somewhat complex render
127
+ /// order: it first resize the plugin's `<div>` without moving it,
128
+ /// using `overflow: hidden` to hide the extra draw area; then,
129
+ /// after the _async_ drawing of the plugin is complete, it will send a
130
+ /// message to complete the toggle action and re-render the element with
131
+ /// the settings removed.
132
+ ///
133
+ /// # Arguments
134
+ /// * `force` - Whether to explicitly set the settings panel state to
135
+ /// Open/Close (`Some(true)`/`Some(false)`), or to just toggle the current
136
+ /// state (`None`).
137
+ fn init_toggle_settings_task(
138
+ &mut self,
139
+ ctx: &Context<Self>,
140
+ force: Option<bool>,
141
+ sender: Option<Sender<ApiResult<JsValue>>>,
142
+ ) {
143
+ let is_open = ctx.props().presentation.is_settings_open();
144
+ ctx.props().presentation.set_settings_before_open(!is_open);
145
+ match force {
146
+ Some(force) if is_open == force => {
147
+ if let Some(sender) = sender {
148
+ sender.send(Ok(JsValue::UNDEFINED)).unwrap();
149
+ }
150
+ },
151
+ Some(_) | None => {
152
+ let force = !is_open;
153
+ let callback = ctx.link().callback(move |resolve| {
154
+ let update = SettingsUpdate::Update(force);
155
+ ToggleSettingsComplete(update, resolve)
156
+ });
157
+
158
+ // P2: at CLOSE time the open-state geometry is measurable —
159
+ // cache the deltas the next OPEN needs to presize with.
160
+ if is_open {
161
+ self.settings_geometry.open_deltas =
162
+ measure_settings_open_deltas(&ctx.props().elem);
163
+ }
164
+
165
+ let open_deltas = self.settings_geometry.open_deltas;
166
+ let workspace = ctx.props().workspace.clone();
167
+ let presentation = ctx.props().presentation.clone();
168
+ let elem = ctx.props().elem.clone();
169
+
170
+ ApiFuture::spawn(async move {
171
+ // Resize every visible plugin (not just the active one) to its
172
+ // new cell as the settings pane toggles. The pane is the outer
173
+ // SplitPanel, which emits no `before-resize`, so this is driven
174
+ // explicitly here. CLOSING grows the cells, so pre-size each
175
+ // plugin to its grown box BEFORE collapsing the pane; OPENING
176
+ // shrinks them, so pre-size to the shrunk box (from the deltas
177
+ // cached at the last close; the first-ever open has none and
178
+ // stays reactive) BEFORE the pane mounts — either way, one
179
+ // clean resize with the reactive pass after as the exactness
180
+ // finalizer.
181
+ let result: ApiResult<JsValue> = async {
182
+ if is_open {
183
+ let presents = presize_visible_panels_grown(&workspace, &elem).await;
184
+ let (notify, rendered) = channel::<()>();
185
+ callback.emit(notify);
186
+ presentation.set_settings_open(false);
187
+ rendered.await?;
188
+ // `notify` fires in `rendered()` (same task as the
189
+ // Yew DOM patch) and this future resumes as one of
190
+ // its microtasks — so the staged reveal below and
191
+ // the pane's geometry change reach the screen in a
192
+ // single paint.
193
+ presents.reveal();
194
+ // I6: the exactness-finalizer resize is part of
195
+ // what this toggle caused — await it here rather
196
+ // than leaving it to the ResizeObserver (whose
197
+ // continuous pass remains as the reactive
198
+ // backstop).
199
+ resize_visible_panels(&workspace).await;
200
+ } else {
201
+ let presents = if let Some((delta_w, delta_h)) = open_deltas {
202
+ presize_visible_panels_open(&workspace, &elem, delta_w, delta_h)
203
+ .await
204
+ } else {
205
+ StagedPresents::default()
206
+ };
207
+
208
+ let (notify, rendered) = channel::<()>();
209
+ callback.emit(notify);
210
+ presentation.set_settings_open(true);
211
+ rendered.await?;
212
+ presents.reveal();
213
+ resize_visible_panels(&workspace).await;
214
+ }
215
+ Ok(JsValue::UNDEFINED)
216
+ }
217
+ .await;
218
+
219
+ if let Some(sender) = sender {
220
+ let msg = result.ignore_view_delete();
221
+ sender
222
+ .send(msg.map(|x| x.unwrap_or(JsValue::UNDEFINED)))
223
+ .into_apierror()?;
224
+ };
225
+
226
+ Ok(JsValue::undefined())
227
+ });
228
+ },
229
+ };
230
+ }
231
+
232
+ pub(super) fn on_settings_panel_size_update(&mut self, x: Option<i32>) -> bool {
233
+ match x {
234
+ Some(x) => {
235
+ self.settings_geometry.pane_width_override = Some(x);
236
+ false
237
+ },
238
+ None => {
239
+ self.settings_geometry.pane_width_override = None;
240
+ self.settings_geometry.auto_width = 0.0;
241
+ self.on_settings_panel_dimensions_reset.emit(());
242
+ true
243
+ },
244
+ }
245
+ }
246
+
247
+ pub(super) fn on_settings_divider_move(
248
+ &mut self,
249
+ ctx: &Context<Self>,
250
+ pane_width: i32,
251
+ ) -> bool {
252
+ // Latest-wins: overwrite the pending target; start the pump if
253
+ // idle. Intermediate targets that arrive while a presize is in
254
+ // flight are dropped (mirrors `PresizeQueue`'s single queued
255
+ // slot) — the pane tracks the pointer at content-render rate.
256
+ self.settings_geometry.divider_target = Some(pane_width);
257
+ if !self.settings_geometry.divider_pumping {
258
+ self.settings_geometry.divider_pumping = true;
259
+ ctx.link().send_message(SettingsDividerPump);
260
+ }
261
+
262
+ false
263
+ }
264
+
265
+ pub(super) fn on_settings_divider_pump(&mut self, ctx: &Context<Self>) -> bool {
266
+ if let Some(pane_width) = self.settings_geometry.divider_target.take() {
267
+ let workspace = ctx.props().workspace.clone();
268
+ let elem = ctx.props().elem.clone();
269
+ let link = ctx.link().clone();
270
+ ApiFuture::spawn(async move {
271
+ let presents =
272
+ presize_visible_panels_pane_width(&workspace, &elem, pane_width as f64).await;
273
+ link.send_message(SettingsDividerCommit(pane_width));
274
+ // Same task as the commit's re-render (whether Yew drained it
275
+ // synchronously inside `send_message` or deferred it to a
276
+ // microtask): the staged reveal and the pane-width geometry
277
+ // land in one paint.
278
+ presents.reveal();
279
+ Ok(())
280
+ });
281
+ } else {
282
+ self.settings_geometry.divider_pumping = false;
283
+ }
284
+
285
+ false
286
+ }
287
+
288
+ pub(super) fn on_settings_divider_commit(
289
+ &mut self,
290
+ ctx: &Context<Self>,
291
+ pane_width: i32,
292
+ ) -> bool {
293
+ // Every visible panel has rendered at (approximately) its
294
+ // target box — NOW move the geometry: the re-render below
295
+ // applies this width to the deferred `SplitPanel`'s controlled
296
+ // `size`, in the same task as the presizes' inline clears.
297
+ self.settings_geometry.pane_width_override = Some(pane_width);
298
+ ctx.link().send_message(SettingsDividerPump);
299
+ true
300
+ }
301
+
302
+ pub(super) fn on_settings_divider_finish(&mut self, ctx: &Context<Self>) -> bool {
303
+ let workspace = ctx.props().workspace.clone();
304
+ ApiFuture::spawn(async move {
305
+ resize_visible_panels(&workspace).await;
306
+ Ok(())
307
+ });
308
+
309
+ false
310
+ }
311
+
312
+ pub(super) fn on_settings_panel_tab_changed(&mut self, tab: SelectedTab) -> bool {
313
+ let changed = tab != self.settings_geometry.selected_tab;
314
+ self.settings_geometry.selected_tab = tab;
315
+ changed
316
+ }
317
+
318
+ pub(super) fn on_settings_panel_auto_width(&mut self, w: f64) -> bool {
319
+ if w > self.settings_geometry.auto_width {
320
+ self.settings_geometry.auto_width = w;
321
+ true
322
+ } else {
323
+ false
324
+ }
325
+ }
326
+
327
+ pub(super) fn on_open_column_settings(
328
+ &mut self,
329
+ ctx: &Context<Self>,
330
+ locator: Option<ColumnLocator>,
331
+ sender: Option<Sender<()>>,
332
+ toggle: bool,
333
+ ) -> bool {
334
+ let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
335
+ if locator == open_column_settings.locator {
336
+ if toggle {
337
+ ctx.props().presentation.set_open_column_settings(None);
338
+ }
339
+ } else {
340
+ open_column_settings.locator.clone_from(&locator);
341
+ open_column_settings.tab = if matches!(locator, Some(ColumnLocator::NewExpression)) {
342
+ Some(ColumnSettingsTab::Attributes)
343
+ } else {
344
+ locator.as_ref().and_then(|x| {
345
+ x.name().map(|x| {
346
+ if self.session_props.is_column_active(x) {
347
+ ColumnSettingsTab::Style
348
+ } else {
349
+ ColumnSettingsTab::Attributes
350
+ }
351
+ })
352
+ })
353
+ };
354
+
355
+ ctx.props()
356
+ .presentation
357
+ .set_open_column_settings(Some(open_column_settings));
358
+
359
+ if locator.is_some() {
360
+ self.settings_geometry.selected_tab = SelectedTab::Query;
361
+ }
362
+ }
363
+
364
+ if let Some(sender) = sender {
365
+ // I6: resolve on the render commit that applies this change (the
366
+ // shared `on_rendered` queue, drained in `rendered()` once fonts
367
+ // settle), not at message-handling time.
368
+ self.on_rendered.push(sender);
369
+ }
370
+
371
+ true
372
+ }
373
+
374
+ pub(super) fn on_column_settings_panel_size_update(&mut self, x: Option<i32>) -> bool {
375
+ self.settings_geometry.column_settings_width_override = x;
376
+ false
377
+ }
378
+
379
+ pub(super) fn on_column_settings_tab_changed(
380
+ &mut self,
381
+ ctx: &Context<Self>,
382
+ tab: ColumnSettingsTab,
383
+ ) -> bool {
384
+ let mut open_column_settings = ctx.props().presentation.get_open_column_settings();
385
+ open_column_settings.tab.clone_from(&Some(tab));
386
+ ctx.props()
387
+ .presentation
388
+ .set_open_column_settings(Some(open_column_settings));
389
+ true
390
+ }
391
+
392
+ /// Toggling the debug tab re-renders the settings panel only — the
393
+ /// `DebugPanel` populates itself (`get_viewer_config` on mount +
394
+ /// change subscriptions), so no plugin dispatch is owed. The old
395
+ /// `just_render` here relied on the pre-amendment unconditional
396
+ /// `Unchanged → update` arm and repainted the plugin as a side effect
397
+ /// (`PLUGIN_DRAW_INVARIANT_PLAN.md` amendment, migration 2).
398
+ pub(super) fn on_toggle_debug(&mut self, _ctx: &Context<Self>) -> bool {
399
+ self.debug_open = !self.debug_open;
400
+ true
401
+ }
402
+ }