@perspective-dev/viewer 4.3.0 → 4.4.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 +2 -2
  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/pro-dark.css +1 -1
  17. package/dist/css/pro.css +1 -1
  18. package/dist/css/solarized-dark.css +1 -1
  19. package/dist/css/solarized.css +1 -1
  20. package/dist/css/themes.css +1 -1
  21. package/dist/css/vaporwave.css +1 -1
  22. package/dist/esm/perspective-viewer.inline.js +2 -2
  23. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  24. package/dist/esm/perspective-viewer.js +2 -2
  25. package/dist/esm/perspective-viewer.js.map +4 -4
  26. package/dist/wasm/perspective-viewer.d.ts +57 -53
  27. package/dist/wasm/perspective-viewer.js +190 -165
  28. package/dist/wasm/perspective-viewer.wasm +0 -0
  29. package/dist/wasm/perspective-viewer.wasm.d.ts +17 -18
  30. package/package.json +7 -5
  31. package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
  32. package/src/css/column-dropdown.css +109 -0
  33. package/src/{less/column-selector.less → css/column-selector.css} +160 -158
  34. package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +69 -59
  35. package/src/{less/column-style.less → css/column-style.css} +52 -66
  36. package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
  37. package/src/{less/config-selector.less → css/config-selector.css} +151 -135
  38. package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
  39. package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
  40. package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
  41. package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
  42. package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
  43. package/src/css/dom/checkbox.css +102 -0
  44. package/src/css/dom/scrollbar.css +35 -0
  45. package/src/{less/dom/select.less → css/dom/select.css} +17 -18
  46. package/src/{less/empty-column.less → css/empty-column.css} +19 -18
  47. package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
  48. package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
  49. package/src/{less/filter-item.less → css/filter-item.css} +16 -15
  50. package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
  51. package/src/{less/form/debug.less → css/form/debug.css} +19 -18
  52. package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
  53. package/src/css/plugin-selector.css +261 -0
  54. package/src/{less/render-warning.less → css/render-warning.css} +18 -17
  55. package/src/{less/status-bar.less → css/status-bar.css} +156 -144
  56. package/src/css/type-icon.css +116 -0
  57. package/src/{less/viewer.less → css/viewer.css} +112 -146
  58. package/src/rust/components/column_dropdown.rs +229 -119
  59. package/src/rust/components/column_selector/active_column.rs +81 -62
  60. package/src/rust/components/column_selector/add_expression_button.rs +1 -0
  61. package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
  62. package/src/rust/components/column_selector/config_selector.rs +315 -199
  63. package/src/rust/components/column_selector/empty_column.rs +2 -2
  64. package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
  65. package/src/rust/components/column_selector/filter_column.rs +37 -26
  66. package/src/rust/components/column_selector/inactive_column.rs +41 -29
  67. package/src/rust/components/column_selector/invalid_column.rs +7 -18
  68. package/src/rust/components/column_selector/pivot_column.rs +11 -5
  69. package/src/rust/components/column_selector/sort_column.rs +23 -13
  70. package/src/rust/components/column_selector.rs +163 -84
  71. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
  72. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
  73. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
  74. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
  75. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
  76. package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
  77. package/src/rust/components/column_settings_sidebar.rs +91 -53
  78. package/src/rust/components/containers/dragdrop_list.rs +2 -1
  79. package/src/rust/components/containers/sidebar_close_button.rs +1 -1
  80. package/src/rust/components/containers/split_panel.rs +1 -0
  81. package/src/rust/components/containers/tab_list.rs +1 -1
  82. package/src/rust/components/copy_dropdown.rs +7 -28
  83. package/src/rust/components/datetime_column_style/custom.rs +2 -2
  84. package/src/rust/components/datetime_column_style/simple.rs +2 -2
  85. package/src/rust/components/datetime_column_style.rs +4 -2
  86. package/src/rust/components/editable_header.rs +7 -4
  87. package/src/rust/components/empty_row.rs +1 -1
  88. package/src/rust/components/export_dropdown.rs +4 -30
  89. package/src/rust/components/expression_editor.rs +19 -10
  90. package/src/rust/components/filter_dropdown.rs +246 -102
  91. package/src/rust/components/font_loader.rs +11 -28
  92. package/src/rust/components/form/code_editor.rs +17 -2
  93. package/src/rust/components/form/color_range_selector.rs +19 -6
  94. package/src/rust/components/form/debug.rs +30 -13
  95. package/src/rust/components/function_dropdown.rs +186 -113
  96. package/src/rust/components/main_panel.rs +71 -89
  97. package/src/rust/components/mod.rs +1 -1
  98. package/src/rust/components/modal.rs +7 -1
  99. package/src/rust/components/number_column_style.rs +22 -7
  100. package/src/rust/components/plugin_selector.rs +34 -102
  101. package/src/rust/components/portal.rs +274 -0
  102. package/src/rust/components/render_warning.rs +72 -123
  103. package/src/rust/components/settings_panel.rs +115 -11
  104. package/src/rust/components/status_bar.rs +222 -98
  105. package/src/rust/components/status_bar_counter.rs +8 -20
  106. package/src/rust/components/status_indicator.rs +64 -114
  107. package/src/rust/components/string_column_style.rs +2 -2
  108. package/src/rust/components/style/style_cache.rs +5 -1
  109. package/src/rust/components/viewer.rs +391 -39
  110. package/src/rust/custom_elements/copy_dropdown.rs +102 -21
  111. package/src/rust/custom_elements/export_dropdown.rs +102 -20
  112. package/src/rust/custom_elements/mod.rs +0 -7
  113. package/src/rust/custom_elements/modal.rs +7 -103
  114. package/src/rust/custom_elements/viewer.rs +99 -35
  115. package/src/rust/custom_events.rs +23 -2
  116. package/src/rust/dragdrop.rs +149 -10
  117. package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
  118. package/src/rust/js/plugin.rs +1 -1
  119. package/src/rust/lib.rs +5 -4
  120. package/src/rust/presentation/props.rs +39 -0
  121. package/src/rust/presentation/sheets.rs +3 -3
  122. package/src/rust/presentation.rs +44 -8
  123. package/src/rust/renderer/limits.rs +32 -3
  124. package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
  125. package/src/rust/renderer.rs +83 -9
  126. package/src/rust/session/column_defaults_update.rs +1 -1
  127. package/src/rust/session/metadata.rs +23 -2
  128. package/src/rust/session/props.rs +178 -0
  129. package/src/rust/session.rs +124 -117
  130. package/src/rust/tasks/column_locator.rs +133 -0
  131. package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
  132. package/src/rust/{model → tasks}/edit_expression.rs +34 -10
  133. package/src/rust/{model → tasks}/eject.rs +2 -2
  134. package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
  135. package/src/rust/{model → tasks}/intersection_observer.rs +19 -3
  136. package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
  137. package/src/rust/tasks/mod.rs +52 -0
  138. package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
  139. package/src/rust/{model → tasks}/resize_observer.rs +39 -6
  140. package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
  141. package/src/rust/tasks/structural.rs +53 -0
  142. package/src/rust/utils/mod.rs +4 -0
  143. package/src/rust/utils/modal_position.rs +110 -0
  144. package/src/rust/utils/ptr_eq_rc.rs +74 -0
  145. package/src/rust/utils/pubsub.rs +11 -1
  146. package/src/svg/bg-pattern.png +0 -0
  147. package/src/svg/close-icon.svg +1 -1
  148. package/src/svg/expression.svg +1 -1
  149. package/src/svg/mega-menu-icons-candlestick.svg +1 -1
  150. package/src/svg/mega-menu-icons-datagrid.svg +1 -2
  151. package/src/svg/mega-menu-icons-heatmap.svg +1 -1
  152. package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
  153. package/src/svg/mega-menu-icons-ohlc.svg +1 -1
  154. package/src/svg/mega-menu-icons-sunburst.svg +1 -1
  155. package/src/svg/mega-menu-icons-treemap.svg +1 -1
  156. package/src/svg/mega-menu-icons-x-bar.svg +1 -1
  157. package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
  158. package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
  159. package/src/svg/mega-menu-icons-y-area.svg +1 -1
  160. package/src/svg/mega-menu-icons-y-bar.svg +1 -1
  161. package/src/svg/mega-menu-icons-y-line.svg +1 -1
  162. package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
  163. package/src/svg/radio-hover.svg +1 -1
  164. package/src/svg/radio-off.svg +1 -1
  165. package/src/svg/radio-on.svg +1 -1
  166. package/src/themes/botanical.css +157 -0
  167. package/src/themes/defaults.css +139 -0
  168. package/src/themes/dracula.css +233 -0
  169. package/src/themes/gruvbox-dark.css +255 -0
  170. package/src/themes/gruvbox.css +134 -0
  171. package/src/themes/icons.css +124 -0
  172. package/src/themes/intl/de.css +102 -0
  173. package/src/themes/intl/es.css +102 -0
  174. package/src/themes/intl/fr.css +102 -0
  175. package/src/themes/intl/ja.css +102 -0
  176. package/src/themes/intl/pt.css +102 -0
  177. package/src/themes/intl/zh.css +102 -0
  178. package/src/themes/intl.css +102 -0
  179. package/src/themes/monokai.css +233 -0
  180. package/src/themes/pro-dark.css +158 -0
  181. package/src/themes/{themes.less → pro.css} +17 -21
  182. package/src/themes/solarized-dark.css +135 -0
  183. package/src/themes/solarized.css +95 -0
  184. package/src/themes/themes.css +22 -0
  185. package/src/themes/vaporwave.css +256 -0
  186. package/dist/css/variables.css +0 -0
  187. package/src/less/column-dropdown.less +0 -95
  188. package/src/less/dom/checkbox.less +0 -100
  189. package/src/less/plugin-selector.less +0 -183
  190. package/src/less/type-icon.less +0 -68
  191. package/src/rust/components/error_message.rs +0 -56
  192. package/src/rust/custom_elements/column_dropdown.rs +0 -123
  193. package/src/rust/custom_elements/filter_dropdown.rs +0 -179
  194. package/src/rust/custom_elements/function_dropdown.rs +0 -115
  195. package/src/rust/model/column_locator.rs +0 -82
  196. package/src/rust/model/is_invalid_drop.rs +0 -36
  197. package/src/rust/model/mod.rs +0 -100
  198. package/src/rust/model/reset_all.rs +0 -38
  199. package/src/rust/model/structural.rs +0 -244
  200. package/src/themes/botanical.less +0 -142
  201. package/src/themes/dracula.less +0 -101
  202. package/src/themes/gruvbox-dark.less +0 -116
  203. package/src/themes/gruvbox.less +0 -152
  204. package/src/themes/icons.less +0 -130
  205. package/src/themes/intl/de.less +0 -102
  206. package/src/themes/intl/es.less +0 -102
  207. package/src/themes/intl/fr.less +0 -102
  208. package/src/themes/intl/ja.less +0 -102
  209. package/src/themes/intl/pt.less +0 -102
  210. package/src/themes/intl/zh.less +0 -102
  211. package/src/themes/intl.less +0 -102
  212. package/src/themes/monokai.less +0 -107
  213. package/src/themes/pro-dark.less +0 -147
  214. package/src/themes/pro.less +0 -186
  215. package/src/themes/solarized-dark.less +0 -78
  216. package/src/themes/solarized.less +0 -102
  217. package/src/themes/vaporwave.less +0 -145
  218. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline0.js +0 -0
  219. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline1.js +0 -0
  220. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline2.js +0 -0
  221. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline3.js +0 -0
  222. /package/dist/wasm/snippets/{perspective-viewer-d729f682ba5c19df → perspective-viewer-68fef752754ffbc6}/inline4.js +0 -0
  223. /package/src/rust/{model → tasks}/copy_export.rs +0 -0
  224. /package/src/rust/{model → tasks}/export_app.rs +0 -0
  225. /package/src/rust/{model → tasks}/export_method.rs +0 -0
  226. /package/src/rust/{model → tasks}/restore_and_render.rs +0 -0
  227. /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
@@ -10,26 +10,27 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use std::rc::Rc;
14
-
13
+ use wasm_bindgen_futures::spawn_local;
15
14
  use web_sys::*;
16
15
  use yew::prelude::*;
17
16
 
18
17
  use super::status_indicator::StatusIndicator;
19
18
  use super::style::LocalStyle;
20
19
  use crate::components::containers::select::*;
20
+ use crate::components::copy_dropdown::CopyDropDownMenu;
21
+ use crate::components::export_dropdown::ExportDropDownMenu;
22
+ use crate::components::portal::PortalModal;
21
23
  use crate::components::status_bar_counter::StatusBarRowsCounter;
22
- use crate::custom_elements::copy_dropdown::*;
23
- use crate::custom_elements::export_dropdown::*;
24
24
  use crate::custom_events::CustomEvents;
25
- use crate::model::*;
25
+ use crate::js::*;
26
26
  use crate::presentation::Presentation;
27
27
  use crate::renderer::*;
28
28
  use crate::session::*;
29
+ use crate::tasks::*;
29
30
  use crate::utils::*;
30
31
  use crate::*;
31
32
 
32
- #[derive(Properties, PerspectiveProperties!)]
33
+ #[derive(Clone, Properties)]
33
34
  pub struct StatusBarProps {
34
35
  // DOM Attribute
35
36
  pub id: String,
@@ -41,6 +42,25 @@ pub struct StatusBarProps {
41
42
  #[prop_or_default]
42
43
  pub on_settings: Option<Callback<()>>,
43
44
 
45
+ // Value props threaded from the root's `SessionProps`.
46
+ // Using these avoids PubSub subscriptions for table_loaded / table_errored.
47
+ pub has_table: Option<TableLoadState>,
48
+ pub is_errored: bool,
49
+ pub stats: Option<ViewStats>,
50
+ /// In-flight render counter and full error, threaded to `StatusIndicator`.
51
+ pub update_count: u32,
52
+ pub error: Option<TableErrorState>,
53
+ /// Title string from session — threaded to avoid title_changed
54
+ /// subscription.
55
+ pub title: Option<String>,
56
+ /// Theme state from presentation — threaded to avoid theme_config_updated /
57
+ /// visibility_changed subscriptions.
58
+ pub is_settings_open: bool,
59
+ pub selected_theme: Option<String>,
60
+ pub available_themes: PtrEqRc<Vec<String>>,
61
+ /// Whether this viewer is hosted inside a `<perspective-workspace>`.
62
+ pub is_workspace: bool,
63
+
44
64
  // State
45
65
  pub custom_events: CustomEvents,
46
66
  pub session: Session,
@@ -51,6 +71,48 @@ pub struct StatusBarProps {
51
71
  impl PartialEq for StatusBarProps {
52
72
  fn eq(&self, other: &Self) -> bool {
53
73
  self.id == other.id
74
+ && self.has_table == other.has_table
75
+ && self.is_errored == other.is_errored
76
+ && self.stats == other.stats
77
+ && self.update_count == other.update_count
78
+ && self.error == other.error
79
+ && self.title == other.title
80
+ && self.is_settings_open == other.is_settings_open
81
+ && self.selected_theme == other.selected_theme
82
+ && self.available_themes == other.available_themes
83
+ && self.is_workspace == other.is_workspace
84
+ }
85
+ }
86
+
87
+ impl HasCustomEvents for StatusBarProps {
88
+ fn custom_events(&self) -> &CustomEvents {
89
+ &self.custom_events
90
+ }
91
+ }
92
+
93
+ impl HasPresentation for StatusBarProps {
94
+ fn presentation(&self) -> &Presentation {
95
+ &self.presentation
96
+ }
97
+ }
98
+
99
+ impl HasRenderer for StatusBarProps {
100
+ fn renderer(&self) -> &Renderer {
101
+ &self.renderer
102
+ }
103
+ }
104
+
105
+ impl HasSession for StatusBarProps {
106
+ fn session(&self) -> &Session {
107
+ &self.session
108
+ }
109
+ }
110
+
111
+ impl StateProvider for StatusBarProps {
112
+ type State = StatusBarProps;
113
+
114
+ fn clone_state(&self) -> Self::State {
115
+ self.clone()
54
116
  }
55
117
  }
56
118
 
@@ -58,9 +120,10 @@ pub enum StatusBarMsg {
58
120
  Reset(MouseEvent),
59
121
  Export,
60
122
  Copy,
123
+ CloseExport,
124
+ CloseCopy,
61
125
  Noop,
62
126
  Eject,
63
- SetThemeConfig((Rc<Vec<String>>, Option<usize>)),
64
127
  SetTheme(String),
65
128
  ResetTheme,
66
129
  PointerEvent(web_sys::PointerEvent),
@@ -70,14 +133,16 @@ pub enum StatusBarMsg {
70
133
 
71
134
  /// A toolbar with buttons, and `Table` & `View` status information.
72
135
  pub struct StatusBar {
73
- _subscriptions: [Subscription; 5],
74
136
  copy_ref: NodeRef,
75
137
  export_ref: NodeRef,
76
138
  input_ref: NodeRef,
77
139
  statusbar_ref: NodeRef,
78
- theme: Option<String>,
79
- themes: Rc<Vec<String>>,
140
+ /// Local title tracks the live `<input>` value before the user commits the
141
+ /// change (blur / Enter). Reset to the prop value whenever the prop
142
+ /// changes.
80
143
  title: Option<String>,
144
+ copy_target: Option<HtmlElement>,
145
+ export_target: Option<HtmlElement>,
81
146
  }
82
147
 
83
148
  impl Component for StatusBar {
@@ -85,21 +150,26 @@ impl Component for StatusBar {
85
150
  type Properties = StatusBarProps;
86
151
 
87
152
  fn create(ctx: &Context<Self>) -> Self {
88
- fetch_initial_theme(ctx);
89
153
  Self {
90
- _subscriptions: register_listeners(ctx),
91
154
  copy_ref: NodeRef::default(),
92
155
  export_ref: NodeRef::default(),
93
156
  input_ref: NodeRef::default(),
94
157
  statusbar_ref: NodeRef::default(),
95
- theme: None,
96
- themes: vec![].into(),
97
- title: ctx.props().session().get_title().clone(),
158
+ title: ctx.props().title.clone(),
159
+ copy_target: None,
160
+ export_target: None,
98
161
  }
99
162
  }
100
163
 
101
- fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
102
- self._subscriptions = register_listeners(ctx);
164
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
165
+ // Keep the local title in sync with the prop whenever the session title
166
+ // changes externally (e.g. restore() call) or the settings panel opens /
167
+ // closes (which resets the input element).
168
+ if ctx.props().title != old_props.title
169
+ || ctx.props().is_settings_open != old_props.is_settings_open
170
+ {
171
+ self.title = ctx.props().title.clone();
172
+ }
103
173
  true
104
174
  }
105
175
 
@@ -111,47 +181,50 @@ impl Component for StatusBar {
111
181
  false
112
182
  },
113
183
  StatusBarMsg::ResetTheme => {
114
- let state = ctx.props().clone_state();
184
+ let presentation = ctx.props().presentation.clone();
185
+ let session = ctx.props().session.clone();
186
+ let renderer = ctx.props().renderer.clone();
115
187
  ApiFuture::spawn(async move {
116
- state.presentation.reset_theme().await?;
117
- let view = state.session.get_view().into_apierror()?;
118
- state.renderer.restyle_all(&view).await
188
+ presentation.reset_theme().await?;
189
+ let view = session.get_view().into_apierror()?;
190
+ renderer.restyle_all(&view).await
119
191
  });
120
192
  true
121
193
  },
122
- StatusBarMsg::SetThemeConfig((themes, index)) => {
123
- let new_theme = index.and_then(|x| themes.get(x)).cloned();
124
- let should_render = new_theme != self.theme || self.themes != themes;
125
- self.theme = new_theme;
126
- self.themes = themes;
127
- should_render
128
- },
129
194
  StatusBarMsg::SetTheme(theme_name) => {
130
- let state = ctx.props().clone_state();
195
+ let presentation = ctx.props().presentation.clone();
196
+ let session = ctx.props().session.clone();
197
+ let renderer = ctx.props().renderer.clone();
131
198
  ApiFuture::spawn(async move {
132
- state.presentation.set_theme_name(Some(&theme_name)).await?;
133
- let view = state.session.get_view().into_apierror()?;
134
- state.renderer.restyle_all(&view).await
199
+ presentation.set_theme_name(Some(&theme_name)).await?;
200
+ let view = session.get_view().into_apierror()?;
201
+ renderer.restyle_all(&view).await
135
202
  });
136
203
 
137
204
  false
138
205
  },
139
206
  StatusBarMsg::Export => {
140
- let target = self.export_ref.cast::<HtmlElement>().into_apierror()?;
141
- ExportDropDownMenuElement::new_from_model(ctx.props()).open(target);
142
- false
207
+ self.export_target = self.export_ref.cast::<HtmlElement>();
208
+ true
143
209
  },
144
210
  StatusBarMsg::Copy => {
145
- let target = self.copy_ref.cast::<HtmlElement>().into_apierror()?;
146
- CopyDropDownMenuElement::new_from_model(ctx.props()).open(target);
147
- false
211
+ self.copy_target = self.copy_ref.cast::<HtmlElement>();
212
+ true
213
+ },
214
+ StatusBarMsg::CloseExport => {
215
+ self.export_target = None;
216
+ true
217
+ },
218
+ StatusBarMsg::CloseCopy => {
219
+ self.copy_target = None;
220
+ true
148
221
  },
149
222
  StatusBarMsg::Eject => {
150
223
  ctx.props().presentation().on_eject.emit(());
151
224
  false
152
225
  },
153
226
  StatusBarMsg::Noop => {
154
- self.title = ctx.props().session().get_title();
227
+ self.title = ctx.props().title.clone();
155
228
  true
156
229
  },
157
230
  StatusBarMsg::TitleInputEvent => {
@@ -201,16 +274,21 @@ impl Component for StatusBar {
201
274
  ..
202
275
  } = ctx.props();
203
276
 
277
+ let has_table = ctx.props().has_table.clone();
278
+ let is_errored = ctx.props().is_errored;
279
+ let is_settings_open = ctx.props().is_settings_open;
280
+ let title = &ctx.props().title;
281
+
204
282
  let mut is_updating_class_name = classes!();
205
- if session.get_title().is_some() {
283
+ if title.is_some() {
206
284
  is_updating_class_name.push("titled");
207
285
  };
208
286
 
209
- if !presentation.is_settings_open() {
287
+ if !is_settings_open {
210
288
  is_updating_class_name.push(["settings-closed", "titled"]);
211
289
  };
212
290
 
213
- if !session.has_table() {
291
+ if !matches!(has_table, Some(TableLoadState::Loaded)) {
214
292
  is_updating_class_name.push("updating");
215
293
  }
216
294
 
@@ -229,20 +307,58 @@ impl Component for StatusBar {
229
307
  .link()
230
308
  .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent);
231
309
 
232
- let is_menu = session.has_table() && ctx.props().on_settings.as_ref().is_none();
310
+ let is_menu = matches!(has_table, Some(TableLoadState::Loaded))
311
+ && ctx.props().on_settings.as_ref().is_none();
233
312
  let is_title = is_menu
234
- || presentation.get_is_workspace()
235
- || session.get_title().is_some()
236
- || session.is_errored()
313
+ || ctx.props().is_workspace
314
+ || title.is_some()
315
+ || is_errored
237
316
  || presentation.is_active(&self.input_ref.cast::<Element>());
238
317
 
239
- let is_settings = session.get_title().is_some()
240
- || presentation.get_is_workspace()
241
- || !session.has_table()
242
- || session.is_errored()
243
- || presentation.is_settings_open()
318
+ let is_settings = title.is_some()
319
+ || ctx.props().is_workspace
320
+ || !matches!(has_table, Some(TableLoadState::Loaded))
321
+ || is_errored
322
+ || is_settings_open
244
323
  || presentation.is_active(&self.input_ref.cast::<Element>());
245
324
 
325
+ let on_copy_select = {
326
+ let props = ctx.props().clone();
327
+ let link = ctx.link().clone();
328
+ Callback::from(move |x: ExportFile| {
329
+ let props = props.clone();
330
+ let link = link.clone();
331
+ spawn_local(async move {
332
+ let mime = x.method.mimetype(x.is_chart);
333
+ let task = props.export_method_to_blob(x.method);
334
+ let result = copy_to_clipboard(task, mime).await;
335
+ crate::maybe_log!({
336
+ result?;
337
+ link.send_message(StatusBarMsg::CloseCopy);
338
+ })
339
+ })
340
+ })
341
+ };
342
+
343
+ let on_export_select = {
344
+ let props = ctx.props().clone();
345
+ let link = ctx.link().clone();
346
+ Callback::from(move |x: ExportFile| {
347
+ if !x.name.is_empty() {
348
+ clone!(props, link);
349
+ spawn_local(async move {
350
+ let val = props.export_method_to_blob(x.method).await.unwrap();
351
+ let is_chart = props.renderer().is_chart();
352
+ download(&x.as_filename(is_chart), &val).unwrap();
353
+ link.send_message(StatusBarMsg::CloseExport);
354
+ })
355
+ }
356
+ })
357
+ };
358
+
359
+ let on_close_copy = ctx.link().callback(|_| StatusBarMsg::CloseCopy);
360
+ let on_close_export = ctx.link().callback(|_| StatusBarMsg::CloseExport);
361
+
246
362
  if is_settings {
247
363
  html! {
248
364
  <>
@@ -253,7 +369,15 @@ impl Component for StatusBar {
253
369
  class={is_updating_class_name}
254
370
  {onpointerdown}
255
371
  >
256
- <StatusIndicator {custom_events} {renderer} {session} />
372
+ <StatusIndicator
373
+ {custom_events}
374
+ {renderer}
375
+ {session}
376
+ update_count={ctx.props().update_count}
377
+ error={ctx.props().error.clone()}
378
+ has_table={ctx.props().has_table.clone()}
379
+ stats={ctx.props().stats.clone()}
380
+ />
257
381
  if is_title {
258
382
  <label
259
383
  class="input-sizer"
@@ -272,21 +396,22 @@ impl Component for StatusBar {
272
396
  </label>
273
397
  }
274
398
  if is_title {
275
- <StatusBarRowsCounter {session} />
399
+ <StatusBarRowsCounter stats={ctx.props().stats.clone()} />
276
400
  }
277
401
  <div id="spacer" />
278
402
  if is_menu {
279
403
  <div id="menu-bar" class="section">
280
404
  <ThemeSelector
281
- theme={self.theme.clone()}
282
- themes={self.themes.clone()}
405
+ theme={ctx.props().selected_theme.clone()}
406
+ themes={ctx.props().available_themes.clone()}
283
407
  on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
284
408
  on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
285
409
  />
286
410
  <div id="plugin-settings"><slot name="statusbar-extra" /></div>
287
411
  <span class="hover-target">
288
412
  <span id="reset" class="button" onmousedown={&onreset}>
289
- <span />
413
+ <span class="icon" />
414
+ <span class="icon-label" />
290
415
  </span>
291
416
  </span>
292
417
  <span
@@ -294,14 +419,20 @@ impl Component for StatusBar {
294
419
  class="hover-target"
295
420
  onmousedown={onexport}
296
421
  >
297
- <span id="export" class="button"><span /></span>
422
+ <span id="export" class="button">
423
+ <span class="icon" />
424
+ <span class="icon-label" />
425
+ </span>
298
426
  </span>
299
427
  <span
300
428
  ref={&self.copy_ref}
301
429
  class="hover-target"
302
430
  onmousedown={oncopy}
303
431
  >
304
- <span id="copy" class="button"><span /></span>
432
+ <span id="copy" class="button">
433
+ <span class="icon" />
434
+ <span class="icon-label" />
435
+ </span>
305
436
  </span>
306
437
  </div>
307
438
  }
@@ -310,17 +441,45 @@ impl Component for StatusBar {
310
441
  id="settings_button"
311
442
  class="noselect"
312
443
  onmousedown={x.reform(|_| ())}
313
- />
314
- <div id="close_button" class="noselect" onmousedown={onclose} />
444
+ >
445
+ <span class="icon" />
446
+ </div>
447
+ <div id="close_button" class="noselect" onmousedown={onclose}>
448
+ <span class="icon" />
449
+ </div>
315
450
  }
316
451
  </div>
452
+ <PortalModal
453
+ tag_name="perspective-copy-menu"
454
+ target={self.copy_target.clone()}
455
+ own_focus=true
456
+ on_close={on_close_copy}
457
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
458
+ >
459
+ <CopyDropDownMenu renderer={renderer.clone()} callback={on_copy_select} />
460
+ </PortalModal>
461
+ <PortalModal
462
+ tag_name="perspective-export-menu"
463
+ target={self.export_target.clone()}
464
+ own_focus=true
465
+ on_close={on_close_export}
466
+ theme={ctx.props().selected_theme.clone().unwrap_or_default()}
467
+ >
468
+ <ExportDropDownMenu
469
+ renderer={renderer.clone()}
470
+ session={session.clone()}
471
+ callback={on_export_select}
472
+ />
473
+ </PortalModal>
317
474
  </>
318
475
  }
319
476
  } else if let Some(x) = ctx.props().on_settings.as_ref() {
320
477
  let class = classes!(is_updating_class_name, "floating");
321
478
  html! {
322
479
  <div id={ctx.props().id.clone()} {class}>
323
- <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())} />
480
+ <div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())}>
481
+ <span class="icon" />
482
+ </div>
324
483
  <div id="close_button" class="noselect" onmousedown={&onclose} />
325
484
  </div>
326
485
  }
@@ -330,46 +489,10 @@ impl Component for StatusBar {
330
489
  }
331
490
  }
332
491
 
333
- fn register_listeners(ctx: &Context<StatusBar>) -> [Subscription; 5] {
334
- [
335
- ctx.props()
336
- .presentation()
337
- .theme_config_updated
338
- .add_listener(ctx.link().callback(StatusBarMsg::SetThemeConfig)),
339
- ctx.props()
340
- .presentation()
341
- .visibility_changed
342
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
343
- ctx.props()
344
- .session()
345
- .title_changed
346
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
347
- ctx.props()
348
- .session()
349
- .table_loaded
350
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
351
- ctx.props()
352
- .session()
353
- .table_errored
354
- .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)),
355
- ]
356
- }
357
-
358
- fn fetch_initial_theme(ctx: &Context<StatusBar>) {
359
- ApiFuture::spawn({
360
- let on_theme = ctx.link().callback(StatusBarMsg::SetThemeConfig);
361
- clone!(ctx.props().presentation());
362
- async move {
363
- on_theme.emit(presentation.get_selected_theme_config().await?);
364
- Ok(())
365
- }
366
- });
367
- }
368
-
369
492
  #[derive(Properties, PartialEq)]
370
493
  struct ThemeSelectorProps {
371
494
  pub theme: Option<String>,
372
- pub themes: Rc<Vec<String>>,
495
+ pub themes: PtrEqRc<Vec<String>>,
373
496
  pub on_reset: Callback<()>,
374
497
  pub on_change: Callback<String>,
375
498
  }
@@ -403,6 +526,7 @@ fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
403
526
  onclick={props.on_reset.reform(|_| ())}
404
527
  />
405
528
  <span id="theme" class="button">
529
+ <span class="icon" />
406
530
  <Select<String>
407
531
  id="theme_selector"
408
532
  class="invert"
@@ -10,35 +10,23 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_client::*;
14
13
  use yew::prelude::*;
15
14
 
16
- use crate::PerspectiveProperties;
17
- use crate::session::{Session, ViewStats};
18
- use crate::utils::{AddListener, u32Ext};
15
+ use crate::session::ViewStats;
16
+ use crate::utils::u32Ext;
19
17
 
20
- #[derive(PartialEq, Properties, PerspectiveProperties!)]
18
+ /// Props are now pure value types: no `Session` handle, no PubSub
19
+ /// subscriptions. The parent passes an updated `stats` whenever the
20
+ /// underlying data changes (driven by the root's `UpdateSession` message).
21
+ #[derive(PartialEq, Properties)]
21
22
  pub struct StatusBarRowsCounterProps {
22
- pub session: Session,
23
+ pub stats: Option<ViewStats>,
23
24
  }
24
25
 
25
26
  /// A component to show the current [`Table`]'s dimensions.
26
27
  #[function_component]
27
28
  pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
28
- let stats = use_state_eq(|| props.session.get_table_stats());
29
- use_effect_with(
30
- (props.session.clone(), stats.setter()),
31
- |(session, set_stats)| {
32
- let sub = session.stats_changed.add_listener({
33
- clone!(session, set_stats);
34
- move |_| set_stats.set(session.get_table_stats())
35
- });
36
-
37
- || drop(sub)
38
- },
39
- );
40
-
41
- match props.session.get_table_stats() {
29
+ match props.stats {
42
30
  Some(
43
31
  ViewStats {
44
32
  num_table_cells: Some((tr, tc)),