@perspective-dev/viewer 4.2.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 (240) 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 -0
  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/extensions.d.ts +32 -1
  23. package/dist/esm/perspective-viewer.d.ts +1 -0
  24. package/dist/esm/perspective-viewer.inline.js +2 -2
  25. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  26. package/dist/esm/perspective-viewer.js +2 -2
  27. package/dist/esm/perspective-viewer.js.map +4 -4
  28. package/dist/esm/ts-rs/GroupRollupMode.d.ts +1 -0
  29. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +2 -0
  30. package/dist/wasm/perspective-viewer.d.ts +57 -53
  31. package/dist/wasm/perspective-viewer.js +197 -164
  32. package/dist/wasm/perspective-viewer.wasm +0 -0
  33. package/dist/wasm/perspective-viewer.wasm.d.ts +17 -18
  34. package/package.json +9 -6
  35. package/src/{less/aggregate-selector.less → css/aggregate-selector.css} +23 -20
  36. package/src/css/column-dropdown.css +109 -0
  37. package/src/{less/column-selector.less → css/column-selector.css} +161 -159
  38. package/src/{less/column-settings-panel.less → css/column-settings-panel.css} +69 -59
  39. package/src/{less/column-style.less → css/column-style.css} +52 -66
  40. package/src/{less/column-symbol-attributes.less → css/column-symbol-attributes.css} +15 -14
  41. package/src/css/config-selector.css +441 -0
  42. package/src/{less/containers/dropdown-menu.less → css/containers/dropdown-menu.css} +20 -19
  43. package/src/{less/containers/pairs-list.less → css/containers/pairs-list.css} +13 -12
  44. package/src/{themes/variables.less → css/containers/scroll-panel.css} +25 -22
  45. package/src/{less/containers/split-panel.less → css/containers/split-panel.css} +15 -14
  46. package/src/{less/containers/tabs.less → css/containers/tabs.css} +17 -19
  47. package/src/css/dom/checkbox.css +102 -0
  48. package/src/css/dom/scrollbar.css +35 -0
  49. package/src/{less/dom/select.less → css/dom/select.css} +17 -18
  50. package/src/{less/empty-column.less → css/empty-column.css} +19 -18
  51. package/src/{less/expression-editor.less → css/expression-editor.css} +19 -18
  52. package/src/{less/filter-dropdown.less → css/filter-dropdown.css} +12 -11
  53. package/src/{less/filter-item.less → css/filter-item.css} +16 -15
  54. package/src/{less/form/code-editor.less → css/form/code-editor.css} +26 -30
  55. package/src/{less/form/debug.less → css/form/debug.css} +19 -18
  56. package/src/{less/function-dropdown.less → css/function-dropdown.css} +12 -11
  57. package/src/css/plugin-selector.css +261 -0
  58. package/src/{less/render-warning.less → css/render-warning.css} +18 -17
  59. package/src/{less/status-bar.less → css/status-bar.css} +156 -144
  60. package/src/css/type-icon.css +116 -0
  61. package/src/{less/viewer.less → css/viewer.css} +112 -146
  62. package/src/rust/components/column_dropdown.rs +229 -119
  63. package/src/rust/components/column_selector/active_column.rs +81 -62
  64. package/src/rust/components/column_selector/add_expression_button.rs +1 -0
  65. package/src/rust/components/column_selector/aggregate_selector.rs +25 -15
  66. package/src/rust/components/column_selector/config_selector.rs +374 -185
  67. package/src/rust/components/column_selector/empty_column.rs +2 -2
  68. package/src/rust/components/column_selector/expr_edit_button.rs +8 -2
  69. package/src/rust/components/column_selector/filter_column.rs +37 -26
  70. package/src/rust/components/column_selector/inactive_column.rs +41 -29
  71. package/src/rust/components/column_selector/invalid_column.rs +7 -18
  72. package/src/rust/components/column_selector/pivot_column.rs +21 -10
  73. package/src/rust/components/column_selector/sort_column.rs +23 -13
  74. package/src/rust/components/column_selector.rs +189 -100
  75. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +1 -1
  76. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +3 -2
  77. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +3 -2
  78. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +2 -3
  79. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +7 -1
  80. package/src/rust/components/column_settings_sidebar/style_tab.rs +153 -112
  81. package/src/rust/components/column_settings_sidebar.rs +91 -53
  82. package/src/rust/components/containers/dragdrop_list.rs +29 -7
  83. package/src/rust/components/containers/scroll_panel.rs +8 -1
  84. package/src/rust/components/containers/select.rs +3 -3
  85. package/src/rust/components/containers/sidebar_close_button.rs +1 -1
  86. package/src/rust/components/containers/split_panel.rs +3 -2
  87. package/src/rust/components/containers/tab_list.rs +1 -1
  88. package/src/rust/components/copy_dropdown.rs +7 -28
  89. package/src/rust/components/datetime_column_style/custom.rs +2 -2
  90. package/src/rust/components/datetime_column_style/simple.rs +2 -2
  91. package/src/rust/components/datetime_column_style.rs +4 -2
  92. package/src/rust/components/editable_header.rs +7 -4
  93. package/src/rust/components/empty_row.rs +1 -1
  94. package/src/rust/components/export_dropdown.rs +4 -30
  95. package/src/rust/components/expression_editor.rs +19 -10
  96. package/src/rust/components/filter_dropdown.rs +246 -102
  97. package/src/rust/components/font_loader.rs +11 -28
  98. package/src/rust/components/form/code_editor.rs +17 -2
  99. package/src/rust/components/form/color_range_selector.rs +19 -6
  100. package/src/rust/components/form/debug.rs +30 -13
  101. package/src/rust/components/function_dropdown.rs +186 -113
  102. package/src/rust/components/main_panel.rs +71 -89
  103. package/src/rust/components/mod.rs +1 -1
  104. package/src/rust/components/modal.rs +7 -1
  105. package/src/rust/components/number_column_style.rs +22 -7
  106. package/src/rust/components/plugin_selector.rs +34 -92
  107. package/src/rust/components/portal.rs +274 -0
  108. package/src/rust/components/render_warning.rs +72 -123
  109. package/src/rust/components/settings_panel.rs +115 -11
  110. package/src/rust/components/status_bar.rs +222 -98
  111. package/src/rust/components/status_bar_counter.rs +8 -20
  112. package/src/rust/components/status_indicator.rs +64 -111
  113. package/src/rust/components/string_column_style.rs +2 -2
  114. package/src/rust/components/style/style_cache.rs +5 -1
  115. package/src/rust/components/viewer.rs +391 -39
  116. package/src/rust/custom_elements/copy_dropdown.rs +102 -21
  117. package/src/rust/custom_elements/export_dropdown.rs +102 -20
  118. package/src/rust/custom_elements/mod.rs +0 -7
  119. package/src/rust/custom_elements/modal.rs +7 -103
  120. package/src/rust/custom_elements/viewer.rs +99 -35
  121. package/src/rust/custom_events.rs +23 -2
  122. package/src/rust/dragdrop.rs +149 -10
  123. package/src/{less/containers/scroll-panel.less → rust/engines.rs} +15 -13
  124. package/src/rust/js/plugin.rs +20 -1
  125. package/src/rust/lib.rs +5 -4
  126. package/src/rust/presentation/props.rs +39 -0
  127. package/src/rust/presentation/sheets.rs +3 -3
  128. package/src/rust/presentation.rs +44 -8
  129. package/src/rust/renderer/limits.rs +32 -3
  130. package/src/{less/dom/scrollbar.less → rust/renderer/props.rs} +18 -19
  131. package/src/rust/renderer/registry.rs +8 -1
  132. package/src/rust/renderer.rs +83 -9
  133. package/src/rust/session/column_defaults_update.rs +18 -0
  134. package/src/rust/session/metadata.rs +23 -2
  135. package/src/rust/session/props.rs +178 -0
  136. package/src/rust/session/replace_expression_update.rs +1 -0
  137. package/src/rust/session.rs +124 -117
  138. package/src/rust/tasks/column_locator.rs +133 -0
  139. package/src/rust/{model → tasks}/columns_iter_set.rs +14 -23
  140. package/src/rust/{model → tasks}/edit_expression.rs +34 -10
  141. package/src/rust/{model → tasks}/eject.rs +2 -2
  142. package/src/rust/{model → tasks}/get_viewer_config.rs +0 -11
  143. package/src/rust/{model → tasks}/intersection_observer.rs +22 -4
  144. package/src/{less/containers/radio-list.less → rust/tasks/is_invalid_drop.rs} +21 -14
  145. package/src/rust/tasks/mod.rs +52 -0
  146. package/src/rust/{model → tasks}/plugin_column_styles.rs +69 -46
  147. package/src/rust/{model → tasks}/resize_observer.rs +39 -6
  148. package/src/rust/{model → tasks}/send_plugin_config.rs +1 -1
  149. package/src/rust/tasks/structural.rs +53 -0
  150. package/src/rust/utils/mod.rs +4 -0
  151. package/src/rust/utils/modal_position.rs +110 -0
  152. package/src/rust/utils/ptr_eq_rc.rs +74 -0
  153. package/src/rust/utils/pubsub.rs +11 -1
  154. package/src/svg/bg-pattern.png +0 -0
  155. package/src/svg/close-icon.svg +1 -1
  156. package/src/svg/expression.svg +1 -1
  157. package/src/svg/mega-menu-icons-candlestick.svg +1 -1
  158. package/src/svg/mega-menu-icons-datagrid.svg +1 -2
  159. package/src/svg/mega-menu-icons-heatmap.svg +1 -1
  160. package/src/svg/mega-menu-icons-map-scatter.svg +1 -1
  161. package/src/svg/mega-menu-icons-ohlc.svg +1 -1
  162. package/src/svg/mega-menu-icons-sunburst.svg +1 -1
  163. package/src/svg/mega-menu-icons-treemap.svg +1 -1
  164. package/src/svg/mega-menu-icons-x-bar.svg +1 -1
  165. package/src/svg/mega-menu-icons-x-y-line.svg +1 -1
  166. package/src/svg/mega-menu-icons-x-y-scatter.svg +1 -1
  167. package/src/svg/mega-menu-icons-y-area.svg +1 -1
  168. package/src/svg/mega-menu-icons-y-bar.svg +1 -1
  169. package/src/svg/mega-menu-icons-y-line.svg +1 -1
  170. package/src/svg/mega-menu-icons-y-scatter.svg +1 -1
  171. package/src/svg/radio-hover.svg +1 -1
  172. package/src/svg/radio-off.svg +1 -1
  173. package/src/svg/radio-on.svg +1 -1
  174. package/src/themes/botanical.css +157 -0
  175. package/src/themes/defaults.css +139 -0
  176. package/src/themes/dracula.css +233 -0
  177. package/src/themes/gruvbox-dark.css +255 -0
  178. package/src/themes/gruvbox.css +134 -0
  179. package/src/themes/icons.css +124 -0
  180. package/src/themes/intl/de.css +102 -0
  181. package/src/themes/intl/es.css +102 -0
  182. package/src/themes/intl/fr.css +102 -0
  183. package/src/themes/intl/ja.css +102 -0
  184. package/src/themes/intl/pt.css +102 -0
  185. package/src/themes/intl/zh.css +102 -0
  186. package/src/themes/intl.css +102 -0
  187. package/src/themes/monokai.css +233 -0
  188. package/src/themes/pro-dark.css +158 -0
  189. package/src/themes/{themes.less → pro.css} +17 -20
  190. package/src/themes/solarized-dark.css +135 -0
  191. package/src/themes/solarized.css +95 -0
  192. package/src/themes/themes.css +22 -0
  193. package/src/themes/vaporwave.css +256 -0
  194. package/src/ts/extensions.ts +73 -2
  195. package/src/ts/perspective-viewer.ts +1 -0
  196. package/src/ts/ts-rs/GroupRollupMode.ts +3 -0
  197. package/src/ts/ts-rs/ViewerConfigUpdate.ts +2 -1
  198. package/tsconfig.json +1 -0
  199. package/dist/css/variables.css +0 -0
  200. package/src/less/column-dropdown.less +0 -95
  201. package/src/less/config-selector.less +0 -363
  202. package/src/less/dom/checkbox.less +0 -100
  203. package/src/less/plugin-selector.less +0 -183
  204. package/src/less/type-icon.less +0 -68
  205. package/src/rust/components/error_message.rs +0 -56
  206. package/src/rust/custom_elements/column_dropdown.rs +0 -123
  207. package/src/rust/custom_elements/filter_dropdown.rs +0 -179
  208. package/src/rust/custom_elements/function_dropdown.rs +0 -115
  209. package/src/rust/model/column_locator.rs +0 -82
  210. package/src/rust/model/is_invalid_drop.rs +0 -36
  211. package/src/rust/model/mod.rs +0 -100
  212. package/src/rust/model/reset_all.rs +0 -38
  213. package/src/rust/model/structural.rs +0 -244
  214. package/src/themes/dracula.less +0 -101
  215. package/src/themes/gruvbox-dark.less +0 -116
  216. package/src/themes/gruvbox.less +0 -152
  217. package/src/themes/icons.less +0 -130
  218. package/src/themes/intl/de.less +0 -102
  219. package/src/themes/intl/es.less +0 -102
  220. package/src/themes/intl/fr.less +0 -102
  221. package/src/themes/intl/ja.less +0 -102
  222. package/src/themes/intl/pt.less +0 -102
  223. package/src/themes/intl/zh.less +0 -102
  224. package/src/themes/intl.less +0 -102
  225. package/src/themes/monokai.less +0 -107
  226. package/src/themes/pro-dark.less +0 -147
  227. package/src/themes/pro.less +0 -186
  228. package/src/themes/solarized-dark.less +0 -78
  229. package/src/themes/solarized.less +0 -102
  230. package/src/themes/vaporwave.less +0 -145
  231. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline0.js +0 -0
  232. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline1.js +0 -0
  233. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline2.js +0 -0
  234. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline3.js +0 -0
  235. /package/dist/wasm/snippets/{perspective-viewer-1586156e058be573 → perspective-viewer-68fef752754ffbc6}/inline4.js +0 -0
  236. /package/src/rust/{model → tasks}/copy_export.rs +0 -0
  237. /package/src/rust/{model → tasks}/export_app.rs +0 -0
  238. /package/src/rust/{model → tasks}/export_method.rs +0 -0
  239. /package/src/rust/{model → tasks}/restore_and_render.rs +0 -0
  240. /package/src/rust/{model → tasks}/update_and_render.rs +0 -0
@@ -10,96 +10,52 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use perspective_client::config::ViewConfigUpdate;
14
- use perspective_js::utils::ApiFuture;
15
13
  use yew::prelude::*;
16
14
 
17
- use super::containers::select::*;
18
15
  use super::style::LocalStyle;
19
- use crate::config::*;
20
- use crate::js::*;
21
- use crate::model::*;
22
- use crate::presentation::Presentation;
23
- use crate::renderer::*;
24
- use crate::session::*;
25
- use crate::utils::*;
26
- use crate::{css, *};
27
-
28
- #[derive(Properties, PartialEq, PerspectiveProperties!)]
16
+ use crate::css;
17
+ use crate::utils::PtrEqRc;
18
+
19
+ /// Pure value props — no engine handles, no PubSub subscriptions.
20
+ /// The parent passes updated values whenever the renderer state changes.
21
+ #[derive(Properties, PartialEq)]
29
22
  pub struct PluginSelectorProps {
30
- pub presentation: Presentation,
31
- pub renderer: Renderer,
32
- pub session: Session,
23
+ /// Name of the currently active plugin.
24
+ pub plugin_name: Option<String>,
25
+
26
+ /// Flat list of all registered plugin names (all categories merged).
27
+ pub available_plugins: PtrEqRc<Vec<String>>,
28
+
29
+ /// Called when the user selects a different plugin.
30
+ pub on_select_plugin: Callback<String>,
33
31
  }
34
32
 
35
33
  #[derive(Debug)]
36
34
  pub enum PluginSelectorMsg {
37
35
  ComponentSelectPlugin(String),
38
- RendererSelectPlugin(String),
39
36
  OpenMenu,
40
37
  }
41
38
 
42
39
  use PluginSelectorMsg::*;
43
40
 
44
41
  pub struct PluginSelector {
45
- options: Vec<SelectItem<String>>,
46
42
  is_open: bool,
47
- _plugin_sub: Subscription,
48
43
  }
49
44
 
50
45
  impl Component for PluginSelector {
51
46
  type Message = PluginSelectorMsg;
52
47
  type Properties = PluginSelectorProps;
53
48
 
54
- fn create(ctx: &Context<Self>) -> Self {
55
- let PluginSelectorProps { renderer, .. } = ctx.props();
56
- let options = generate_plugin_optgroups(renderer);
57
- let _plugin_sub = renderer.plugin_changed.add_listener({
58
- let link = ctx.link().clone();
59
- move |plugin: JsPerspectiveViewerPlugin| {
60
- let name = plugin.name();
61
- link.send_message(PluginSelectorMsg::RendererSelectPlugin(name))
62
- }
63
- });
64
-
65
- Self {
66
- options,
67
- is_open: false,
68
- _plugin_sub,
69
- }
49
+ fn create(_ctx: &Context<Self>) -> Self {
50
+ Self { is_open: false }
70
51
  }
71
52
 
72
53
  fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
73
- let PluginSelectorProps {
74
- presentation,
75
- renderer,
76
- session,
77
- ..
78
- } = ctx.props();
79
54
  match msg {
80
- RendererSelectPlugin(_plugin_name) => true,
81
55
  ComponentSelectPlugin(plugin_name) => {
82
- if !session.is_errored() {
83
- let metadata =
84
- renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name));
85
-
86
- let mut update = ViewConfigUpdate::default();
87
- session.set_update_column_defaults(
88
- &mut update,
89
- metadata.as_ref().unwrap_or(&*renderer.metadata()),
90
- );
91
-
92
- if let Ok(task) = ctx.props().update_and_render(update) {
93
- ApiFuture::spawn(task);
94
- }
95
-
96
- presentation.set_open_column_settings(None);
97
- self.is_open = false;
98
- false
99
- } else {
100
- self.is_open = false;
101
- true
102
- }
56
+ ctx.props().on_select_plugin.emit(plugin_name);
57
+ self.is_open = false;
58
+ false
103
59
  },
104
60
  OpenMenu => {
105
61
  self.is_open = !self.is_open;
@@ -114,22 +70,18 @@ impl Component for PluginSelector {
114
70
 
115
71
  fn view(&self, ctx: &Context<Self>) -> Html {
116
72
  let callback = ctx.link().callback(|_| OpenMenu);
117
- let plugin_name = ctx.props().renderer.get_active_plugin().unwrap().name();
73
+ let plugin_name = ctx.props().plugin_name.clone().unwrap_or_default();
118
74
  let plugin_name2 = plugin_name.clone();
119
75
  let class = if self.is_open { "open" } else { "" };
120
- let items = self.options.iter().map(|item| match item {
121
- SelectItem::OptGroup(_cat, items) => html! {
122
- items.iter().filter(|x| *x != &plugin_name2).map(|x| {
123
- let callback = ctx.link().callback(ComponentSelectPlugin);
124
- html! {
125
- <PluginSelect
126
- name={ x.to_owned() }
127
- on_click={ callback } />
128
- }
129
- }).collect::<Html>()
130
- },
131
- SelectItem::Option(_item) => html! {},
132
- });
76
+ let items = ctx
77
+ .props()
78
+ .available_plugins
79
+ .iter()
80
+ .filter(|x| x.as_str() != plugin_name2.as_str())
81
+ .map(|x| {
82
+ let callback = ctx.link().callback(ComponentSelectPlugin);
83
+ html! { <PluginSelect name={x.to_owned()} on_click={callback} /> }
84
+ });
133
85
 
134
86
  html! {
135
87
  <>
@@ -138,7 +90,9 @@ impl Component for PluginSelector {
138
90
  <PluginSelect name={plugin_name} on_click={callback} />
139
91
  <div id="plugin_selector_border" />
140
92
  if self.is_open {
141
- <div class="plugin-selector-options">{ items.collect::<Html>() }</div>
93
+ <div class="plugin-selector-options scrollable">
94
+ { items.collect::<Html>() }
95
+ </div>
142
96
  }
143
97
  </div>
144
98
  </>
@@ -146,19 +100,6 @@ impl Component for PluginSelector {
146
100
  }
147
101
  }
148
102
 
149
- /// Generate the opt groups for the plugin selector by collecting by category
150
- /// then sorting.
151
- fn generate_plugin_optgroups(renderer: &Renderer) -> Vec<SelectItem<String>> {
152
- let mut options = renderer
153
- .get_all_plugin_categories()
154
- .into_iter()
155
- .map(|(category, value)| SelectItem::OptGroup(category.into(), value))
156
- .collect::<Vec<_>>();
157
-
158
- options.sort_by_key(|x| x.name());
159
- options
160
- }
161
-
162
103
  #[derive(Properties, PartialEq)]
163
104
  struct PluginSelectProps {
164
105
  name: String,
@@ -184,9 +125,10 @@ fn PluginSelect(props: &PluginSelectProps) -> Html {
184
125
  <div
185
126
  class="plugin-select-item"
186
127
  data-plugin={name.clone()}
187
- style={format!("--default-column-title:var(--plugin-name-{}--content, \"{}\")", path, props.name)}
128
+ style={format!("--default-column-title:var(--psp-plugin-name--{}--content, \"{}\")", path, props.name)}
188
129
  onclick={props.on_click.reform(move |_| name.clone())}
189
130
  >
131
+ <span class="icon" />
190
132
  <span class="plugin-select-item-name" />
191
133
  </div>
192
134
  }
@@ -0,0 +1,274 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ use std::cell::Cell;
14
+ use std::rc::Rc;
15
+
16
+ use perspective_js::utils::global;
17
+ use wasm_bindgen::JsCast;
18
+ use wasm_bindgen::prelude::*;
19
+ use web_sys::*;
20
+ use yew::prelude::*;
21
+
22
+ use crate::components::modal::ModalOrientation;
23
+ use crate::components::style::StyleProvider;
24
+ use crate::utils::*;
25
+
26
+ #[derive(Properties, PartialEq)]
27
+ pub struct PortalModalProps {
28
+ pub children: Children,
29
+
30
+ /// The element to position relative to. `None` means closed.
31
+ pub target: Option<HtmlElement>,
32
+
33
+ /// Whether the portal manages its own focus and closes on blur.
34
+ #[prop_or(true)]
35
+ pub own_focus: bool,
36
+
37
+ /// Called when the portal closes (blur, etc).
38
+ #[prop_or_default]
39
+ pub on_close: Callback<()>,
40
+
41
+ pub tag_name: &'static str,
42
+
43
+ pub theme: String,
44
+ }
45
+
46
+ pub enum PortalModalMsg {
47
+ Reposition,
48
+ }
49
+
50
+ pub struct PortalModal {
51
+ host: HtmlElement,
52
+ shadow_root: Element,
53
+ top: f64,
54
+ left: f64,
55
+ visible: bool,
56
+ rev_vert: ModalOrientation,
57
+ anchor: Rc<Cell<ModalAnchor>>,
58
+ _blur_closure: Option<Closure<dyn FnMut(FocusEvent)>>,
59
+ }
60
+
61
+ impl PortalModal {
62
+ fn attach_to_body(&self) {
63
+ if !self.host.is_connected() {
64
+ let _ = global::body().append_child(&self.host);
65
+ }
66
+ }
67
+
68
+ fn detach_from_body(&mut self) {
69
+ if self.host.is_connected() {
70
+ let _ = global::body().remove_child(&self.host);
71
+ }
72
+
73
+ if let Some(closure) = self._blur_closure.as_ref() {
74
+ self.host
75
+ .remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
76
+ .unwrap()
77
+ }
78
+
79
+ self._blur_closure = None;
80
+ }
81
+
82
+ fn position_against_target(&mut self, target: &HtmlElement) {
83
+ let target_rect = target.get_bounding_client_rect();
84
+ let height = target_rect.height();
85
+ let width = target_rect.width();
86
+ let top = target_rect.top();
87
+ let left = target_rect.left();
88
+
89
+ if !self.visible {
90
+ // First pass: position at default anchor, invisible
91
+ self.top = top + height - 1.0;
92
+ self.left = left;
93
+ self.visible = false;
94
+ } else {
95
+ // Second pass: compute actual anchor and reposition
96
+ let anchor = calc_relative_position(&self.host, top, left, height, width);
97
+ self.anchor.set(anchor);
98
+ let modal_rect = self.host.get_bounding_client_rect();
99
+ let (new_top, new_left) = calc_anchor_position(anchor, &target_rect, &modal_rect);
100
+ self.top = new_top;
101
+ self.left = new_left;
102
+ self.rev_vert.set(anchor.is_rev_vert());
103
+ }
104
+ }
105
+
106
+ fn setup_blur_handler(&mut self, ctx: &Context<Self>) {
107
+ let on_close = {
108
+ let target = ctx.props().target.clone();
109
+ ctx.props().on_close.reform(move |_| {
110
+ if let Some(target) = &target {
111
+ target.class_list().remove_1("modal-target").unwrap();
112
+ }
113
+ })
114
+ };
115
+
116
+ let closure = Closure::wrap(Box::new(move |_: FocusEvent| {
117
+ on_close.emit(());
118
+ }) as Box<dyn FnMut(FocusEvent)>);
119
+
120
+ let _ = self
121
+ .host
122
+ .add_event_listener_with_callback("blur", closure.as_ref().unchecked_ref());
123
+
124
+ self._blur_closure = Some(closure);
125
+ }
126
+ }
127
+
128
+ impl Component for PortalModal {
129
+ type Message = PortalModalMsg;
130
+ type Properties = PortalModalProps;
131
+
132
+ fn create(ctx: &Context<Self>) -> Self {
133
+ let host: HtmlElement = global::document()
134
+ .create_element(ctx.props().tag_name)
135
+ .unwrap()
136
+ .unchecked_into();
137
+
138
+ host.style().set_property("position", "fixed").unwrap();
139
+ host.style().set_property("z-index", "10000").unwrap();
140
+ let init = ShadowRootInit::new(ShadowRootMode::Open);
141
+ let shadow_root = if let Some(elem) = host.shadow_root() {
142
+ elem
143
+ } else {
144
+ host.attach_shadow(&init).unwrap()
145
+ }
146
+ .unchecked_into::<Element>();
147
+
148
+ Self {
149
+ host,
150
+ shadow_root,
151
+ top: 0.0,
152
+ left: 0.0,
153
+ visible: false,
154
+ rev_vert: Default::default(),
155
+ anchor: Default::default(),
156
+ _blur_closure: None,
157
+ }
158
+ }
159
+
160
+ fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
161
+ match msg {
162
+ PortalModalMsg::Reposition => {
163
+ self.visible = true;
164
+ true
165
+ },
166
+ }
167
+ }
168
+
169
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
170
+ let new_target = &ctx.props().target;
171
+ let old_target = &old_props.target;
172
+
173
+ match (old_target, new_target, self._blur_closure.as_ref()) {
174
+ (None, Some(_), Some(closure)) => {
175
+ self.visible = false;
176
+ self.host
177
+ .remove_event_listener_with_callback("blur", closure.as_ref().unchecked_ref())
178
+ .unwrap();
179
+
180
+ self._blur_closure = None;
181
+ },
182
+ (None, Some(_), None) => {
183
+ self.visible = false;
184
+ self._blur_closure = None;
185
+ },
186
+ (Some(_), None, _) => {
187
+ self.detach_from_body();
188
+ return true;
189
+ },
190
+ _ => {},
191
+ }
192
+
193
+ true
194
+ }
195
+
196
+ fn view(&self, ctx: &Context<Self>) -> Html {
197
+ let target = &ctx.props().target;
198
+ if target.is_none() {
199
+ return html! {};
200
+ }
201
+
202
+ let opacity = if self.visible { "" } else { ";opacity:0" };
203
+ let css = format!(
204
+ ":host{{top:{}px;left:{}px{}}}",
205
+ self.top, self.left, opacity
206
+ );
207
+
208
+ let portal_content = html! {
209
+ <>
210
+ <style>{ css }</style>
211
+ <ContextProvider<ModalOrientation> context={self.rev_vert.clone()}>
212
+ <StyleProvider root={self.host.clone()}>
213
+ { for ctx.props().children.iter() }
214
+ </StyleProvider>
215
+ </ContextProvider<ModalOrientation>>
216
+ </>
217
+ };
218
+
219
+ yew::create_portal(portal_content, self.shadow_root.clone())
220
+ }
221
+
222
+ fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
223
+ if let Some(target) = &ctx.props().target {
224
+ if !self.host.is_connected() {
225
+ let theme = ctx.props().theme.as_str();
226
+ self.host.set_attribute("theme", theme).unwrap();
227
+
228
+ // First render with a target: attach to body, position invisible
229
+ self.position_against_target(target);
230
+ self.attach_to_body();
231
+
232
+ // Propagate theme from target
233
+ if let Some(theme) = target.get_attribute("theme") {
234
+ let _ = self.host.set_attribute("theme", &theme);
235
+ }
236
+
237
+ target.class_list().add_1("modal-target").unwrap();
238
+
239
+ if ctx.props().own_focus {
240
+ self.host.set_attribute("tabindex", "0").unwrap();
241
+ self.setup_blur_handler(ctx);
242
+ }
243
+
244
+ // Schedule second positioning pass
245
+ let link = ctx.link().clone();
246
+ wasm_bindgen_futures::spawn_local(async move {
247
+ request_animation_frame().await;
248
+ link.send_message(PortalModalMsg::Reposition);
249
+ });
250
+ } else if self.visible {
251
+ // Second pass: reposition with correct anchor
252
+ self.position_against_target(target);
253
+
254
+ if ctx.props().own_focus && self._blur_closure.is_some() {
255
+ let _ = self.host.focus();
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ fn destroy(&mut self, ctx: &Context<Self>) {
262
+ if let Some(target) = &ctx.props().target {
263
+ target.class_list().remove_1("modal-target").unwrap();
264
+ if target.get_attribute("theme").is_some() {
265
+ let _ = self.host.remove_attribute("theme");
266
+ }
267
+
268
+ let event = CustomEvent::new("-perspective-close-expression").unwrap();
269
+ let _ = target.dispatch_event(&event);
270
+ }
271
+
272
+ self.detach_from_body();
273
+ }
274
+ }
@@ -13,144 +13,93 @@
13
13
  use yew::prelude::*;
14
14
 
15
15
  use super::style::LocalStyle;
16
- use crate::model::*;
17
- use crate::renderer::*;
18
- use crate::session::*;
19
- use crate::*;
16
+ use crate::css;
17
+ use crate::renderer::limits::RenderLimits;
20
18
 
21
- #[derive(Properties, PerspectiveProperties!)]
19
+ #[derive(Properties, PartialEq)]
22
20
  pub struct RenderWarningProps {
23
- // Current dimensions
24
- pub dimensions: Option<(usize, usize, Option<usize>, Option<usize>)>,
21
+ pub dimensions: Option<RenderLimits>,
25
22
 
26
- // State
27
- pub renderer: Renderer,
28
- pub session: Session,
23
+ /// Called when the user clicks "Render all points". The parent disables
24
+ /// the render warning on the active plugin and re-draws.
25
+ pub on_dismiss: Callback<()>,
29
26
  }
30
27
 
31
- impl PartialEq for RenderWarningProps {
32
- fn eq(&self, other: &Self) -> bool {
33
- self.dimensions == other.dimensions
34
- }
35
- }
36
-
37
- pub enum RenderWarningMsg {
38
- DismissWarning,
39
- }
40
-
41
- pub struct RenderWarning {
42
- col_warn: Option<(usize, usize)>,
43
- row_warn: Option<(usize, usize)>,
44
- }
45
-
46
- impl RenderWarning {
47
- fn update_warnings(&mut self, ctx: &Context<Self>) {
48
- if let Some((num_cols, num_rows, max_cols, max_rows)) = ctx.props().dimensions {
49
- let count = num_cols * num_rows;
50
- if max_cols.is_some_and(|x| x < num_cols) {
51
- self.col_warn = Some((max_cols.unwrap(), num_cols));
52
- } else {
53
- self.col_warn = None;
54
- }
55
-
56
- if max_rows.is_some_and(|x| x < num_rows) {
57
- self.row_warn = Some((num_cols * max_rows.unwrap(), count));
58
- } else {
59
- self.row_warn = None;
60
- }
28
+ #[function_component(RenderWarning)]
29
+ pub fn render_warning(props: &RenderWarningProps) -> Html {
30
+ let dimensions = props.dimensions;
31
+ let (col_warn, row_warn) = if let Some(limits) = dimensions {
32
+ let col_warn = if limits.max_cols.is_some_and(|x| x < limits.num_cols) {
33
+ Some((limits.max_cols.unwrap(), limits.num_cols))
61
34
  } else {
62
- self.col_warn = None;
63
- self.row_warn = None;
64
- }
65
- }
66
- }
67
-
68
- impl Component for RenderWarning {
69
- type Message = RenderWarningMsg;
70
- type Properties = RenderWarningProps;
71
-
72
- fn create(ctx: &Context<Self>) -> Self {
73
- let mut elem = Self {
74
- col_warn: None,
75
- row_warn: None,
35
+ None
76
36
  };
77
37
 
78
- elem.update_warnings(ctx);
79
- elem
80
- }
38
+ let row_warn = if limits.max_rows.is_some_and(|x| x < limits.num_rows) {
39
+ Some((
40
+ limits.num_cols * limits.max_rows.unwrap(),
41
+ limits.num_cols * limits.num_rows,
42
+ ))
43
+ } else {
44
+ None
45
+ };
81
46
 
82
- fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
83
- match msg {
84
- RenderWarningMsg::DismissWarning => {
85
- let state = ctx.props().clone_state();
86
- ApiFuture::spawn(async move {
87
- state.renderer().disable_active_plugin_render_warning();
88
- let view_task = state.session().get_view();
89
- state.renderer().update(view_task).await
90
- });
47
+ (col_warn, row_warn)
48
+ } else {
49
+ (None, None)
50
+ };
51
+
52
+ if col_warn.is_some() || row_warn.is_some() {
53
+ let warning = match (col_warn, row_warn) {
54
+ (Some((x, y)), Some((a, b))) => html! {
55
+ <span style="white-space: nowrap">
56
+ { "Rendering" }
57
+ { render_pair(x, y) }
58
+ { "of columns and" }
59
+ { render_pair(a, b) }
60
+ { "of points." }
61
+ </span>
62
+ },
63
+ (Some((x, y)), None) => html! {
64
+ <span style="white-space: nowrap">
65
+ { "Rendering" }
66
+ { render_pair(x, y) }
67
+ { "of columns." }
68
+ </span>
69
+ },
70
+ (None, Some((x, y))) => html! {
71
+ <span style="white-space: nowrap">
72
+ { "Rendering" }
73
+ { render_pair(x, y) }
74
+ { "of points." }
75
+ </span>
91
76
  },
77
+ _ => html! { <div /> },
92
78
  };
93
- true
94
- }
95
-
96
- fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
97
- self.update_warnings(ctx);
98
- true
99
- }
100
79
 
101
- fn view(&self, ctx: &Context<Self>) -> Html {
102
- if self.col_warn.is_some() || self.row_warn.is_some() {
103
- let warning = match (self.col_warn, self.row_warn) {
104
- (Some((x, y)), Some((a, b))) => html! {
105
- <span style="white-space: nowrap">
106
- { "Rendering" }
107
- { render_pair(x, y) }
108
- { "of columns and" }
109
- { render_pair(a, b) }
110
- { "of points." }
80
+ let on_dismiss = props.on_dismiss.clone();
81
+ let onclick = Callback::from(move |_: MouseEvent| on_dismiss.emit(()));
82
+ html! {
83
+ <>
84
+ <LocalStyle href={css!("render-warning")} />
85
+ <div
86
+ class="plugin_information plugin_information--warning"
87
+ id="plugin_information--size"
88
+ >
89
+ <span class="plugin_information__icon" />
90
+ <span class="plugin_information__text" id="plugin_information_count">
91
+ { warning }
111
92
  </span>
112
- },
113
- (Some((x, y)), None) => html! {
114
- <span style="white-space: nowrap">
115
- { "Rendering" }
116
- { render_pair(x, y) }
117
- { "of columns." }
118
- </span>
119
- },
120
- (None, Some((x, y))) => html! {
121
- <span style="white-space: nowrap">
122
- { "Rendering" }
123
- { render_pair(x, y) }
124
- { "of points." }
125
- </span>
126
- },
127
- _ => html! { <div /> },
128
- };
129
-
130
- let onclick = ctx.link().callback(|_| RenderWarningMsg::DismissWarning);
131
-
132
- html! {
133
- <>
134
- <LocalStyle href={css!("render-warning")} />
135
- <div
136
- class="plugin_information plugin_information--warning"
137
- id="plugin_information--size"
138
- >
139
- <span class="plugin_information__icon" />
140
- <span class="plugin_information__text" id="plugin_information_count">
141
- { warning }
93
+ <span class="plugin_information__actions">
94
+ <span class="plugin_information__action" onmousedown={onclick}>
95
+ { "Render all points" }
142
96
  </span>
143
- <span class="plugin_information__actions">
144
- <span class="plugin_information__action" onmousedown={onclick}>
145
- { "Render all points" }
146
- </span>
147
- </span>
148
- </div>
149
- </>
150
- }
151
- } else {
152
- html! {}
97
+ </span>
98
+ </div>
99
+ </>
153
100
  }
101
+ } else {
102
+ html! {}
154
103
  }
155
104
  }
156
105