@perspective-dev/viewer 5.3.0 → 5.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 (265) hide show
  1. package/dist/cdn/perspective-viewer.js +8 -8
  2. package/dist/cdn/perspective-viewer.js.map +3 -3
  3. package/dist/css/blueprint.css +1 -0
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -0
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -0
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -0
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -0
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -0
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/column-format.d.ts +16 -3
  31. package/dist/esm/extensions.d.ts +0 -5
  32. package/dist/esm/perspective-viewer.inline.js +7 -7
  33. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  34. package/dist/esm/perspective-viewer.js +8 -8
  35. package/dist/esm/perspective-viewer.js.map +3 -3
  36. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  37. package/dist/wasm/perspective-viewer.d.ts +62 -73
  38. package/dist/wasm/perspective-viewer.js +230 -238
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -28
  41. package/package.json +7 -7
  42. package/src/css/_context_menu.css +1 -1
  43. package/src/css/_dropdown_menu.css +1 -1
  44. package/src/css/_viewer_bundle.css +7 -8
  45. package/src/css/column-dropdown.css +0 -3
  46. package/src/css/column-selector.css +8 -5
  47. package/src/css/column-settings-panel.css +0 -206
  48. package/src/css/column-style.css +95 -0
  49. package/src/css/config-selector.css +18 -10
  50. package/src/css/filter-dropdown.css +5 -5
  51. package/src/css/form/debug.css +28 -4
  52. package/src/css/function-dropdown.css +5 -5
  53. package/src/css/panel-tab.css +23 -0
  54. package/src/css/plugin-selector.css +6 -5
  55. package/src/css/render-warning.css +1 -1
  56. package/src/css/status-bar.css +52 -21
  57. package/src/css/type-icon.css +1 -3
  58. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  59. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  60. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  61. package/src/css/{containers → ui/containers}/tabs.css +13 -3
  62. package/src/css/{dom → ui/dom}/select.css +19 -8
  63. package/src/css/viewer.css +32 -31
  64. package/src/rust/agent/tools.rs +5 -2
  65. package/src/rust/components/chat_panel.rs +1 -1
  66. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  67. package/src/rust/components/column_dropdown.rs +2 -2
  68. package/src/rust/components/column_selector/active_column.rs +15 -9
  69. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  70. package/src/rust/components/column_selector/config_selector.rs +2 -2
  71. package/src/rust/components/column_selector/filter_column.rs +8 -5
  72. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  73. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  74. package/src/rust/components/column_selector/sort_column.rs +7 -4
  75. package/src/rust/components/column_selector.rs +3 -3
  76. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  77. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +15 -17
  78. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  79. package/src/rust/components/column_settings_sidebar/style_tab.rs +296 -238
  80. package/src/rust/components/column_settings_sidebar.rs +3 -4
  81. package/src/rust/components/copy_dropdown.rs +1 -1
  82. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  83. package/src/rust/components/datetime_column_style/simple.rs +16 -6
  84. package/src/rust/components/datetime_column_style.rs +51 -12
  85. package/src/rust/components/{form/debug.rs → debug_panel.rs} +5 -5
  86. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  87. package/src/rust/components/export_dropdown.rs +1 -1
  88. package/src/rust/components/expression_editor.rs +1 -1
  89. package/src/rust/components/filter_dropdown.rs +2 -2
  90. package/src/rust/components/function_dropdown.rs +2 -2
  91. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  92. package/src/rust/components/main_panel/reconcile.rs +9 -4
  93. package/src/rust/components/main_panel/render.rs +62 -11
  94. package/src/rust/components/main_panel/update.rs +39 -10
  95. package/src/rust/components/main_panel.rs +26 -2
  96. package/src/rust/components/mod.rs +5 -7
  97. package/src/rust/components/new_panel_menu.rs +97 -0
  98. package/src/rust/components/number_series_style.rs +2 -3
  99. package/src/rust/components/panel_menu.rs +61 -87
  100. package/src/rust/components/panel_tab.rs +100 -43
  101. package/src/rust/components/plugin_tab.rs +125 -87
  102. package/src/rust/components/render_warning.rs +2 -2
  103. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  104. package/src/rust/components/settings_panel.rs +3 -2
  105. package/src/rust/components/status_bar.rs +113 -20
  106. package/src/rust/components/status_indicator.rs +5 -14
  107. package/src/rust/components/string_column_style.rs +1 -2
  108. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  109. package/src/rust/components/style_controls/number_string_format/digits_section.rs +25 -15
  110. package/src/rust/components/style_controls/number_string_format/misc_section.rs +7 -3
  111. package/src/rust/components/style_controls/number_string_format/style_section.rs +2 -1
  112. package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
  113. package/src/rust/components/style_controls/number_string_format.rs +37 -14
  114. package/src/rust/components/viewer/msg.rs +43 -30
  115. package/src/rust/components/viewer/panels.rs +50 -19
  116. package/src/rust/components/viewer/render.rs +15 -13
  117. package/src/rust/components/viewer/settings.rs +137 -68
  118. package/src/rust/components/viewer/snapshots.rs +5 -10
  119. package/src/rust/components/viewer/wiring.rs +1 -21
  120. package/src/rust/components/viewer.rs +15 -10
  121. package/src/rust/components/window_editor.rs +8 -9
  122. package/src/rust/config/column_config_schema.rs +358 -52
  123. package/src/rust/config/datetime_column_style.rs +4 -21
  124. package/src/rust/config/mod.rs +0 -2
  125. package/src/rust/config/number_string_format.rs +211 -18
  126. package/src/rust/config/workspace_config.rs +2 -1
  127. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  128. package/src/rust/custom_elements/mod.rs +0 -3
  129. package/src/rust/custom_elements/viewer.rs +0 -18
  130. package/src/rust/js/plugin.rs +0 -86
  131. package/src/rust/lib.rs +12 -15
  132. package/src/rust/presentation/column_locator.rs +3 -7
  133. package/src/rust/presentation.rs +85 -38
  134. package/src/rust/queries/hosted_tables.rs +38 -0
  135. package/src/rust/queries/mod.rs +2 -0
  136. package/src/rust/queries/palette_set.rs +1 -1
  137. package/src/rust/queries/plugin_column_styles.rs +2 -2
  138. package/src/rust/renderer/activate.rs +1 -3
  139. package/src/rust/renderer/dispatch.rs +117 -30
  140. package/src/rust/renderer/limits.rs +24 -5
  141. package/src/rust/renderer/plugin_config.rs +56 -8
  142. package/src/rust/renderer/props.rs +2 -1
  143. package/src/rust/renderer.rs +14 -27
  144. package/src/rust/session/props.rs +4 -3
  145. package/src/rust/session/view_subscription.rs +19 -6
  146. package/src/rust/session.rs +38 -70
  147. package/src/rust/tasks/create_panel.rs +1 -1
  148. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  149. package/src/rust/tasks/eject.rs +1 -0
  150. package/src/rust/tasks/pipeline.rs +0 -60
  151. package/src/rust/tasks/presize_panels.rs +24 -1
  152. package/src/rust/tasks/restore_and_render.rs +0 -17
  153. package/src/rust/tasks/send_column_config.rs +1 -1
  154. package/src/rust/tasks/send_plugin_config.rs +1 -1
  155. package/src/rust/tasks/set_edit_mode.rs +1 -1
  156. package/src/rust/ui/containers/control_group.rs +113 -0
  157. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  158. package/src/rust/{components → ui}/containers/mod.rs +1 -4
  159. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  160. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  161. package/src/rust/{components → ui}/containers/select.rs +8 -8
  162. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  163. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  164. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  165. package/src/rust/{components → ui}/context_menu.rs +8 -13
  166. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  167. package/src/rust/{components → ui}/font_loader.rs +0 -26
  168. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  169. package/src/rust/ui/form/intl_label.rs +81 -0
  170. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  171. package/src/rust/{components → ui}/form/mod.rs +1 -10
  172. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  173. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  174. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  175. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  176. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  177. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  178. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  179. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  180. package/src/rust/ui/mod.rs +56 -0
  181. package/src/rust/ui/modal.rs +61 -0
  182. package/src/rust/{components → ui}/portal.rs +174 -79
  183. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  184. package/src/rust/utils/completion.rs +0 -6
  185. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  186. package/src/rust/utils/mod.rs +2 -0
  187. package/src/rust/workspace.rs +35 -0
  188. package/src/svg/boolean-type.svg +3 -2
  189. package/src/svg/checkbox-checked-icon.svg +4 -7
  190. package/src/svg/checkbox-hover.svg +3 -3
  191. package/src/svg/checkbox-off.svg +2 -3
  192. package/src/svg/checkbox-on.svg +3 -3
  193. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  194. package/src/svg/datagrid-select-column.svg +8 -15
  195. package/src/svg/datagrid-select-region.svg +8 -15
  196. package/src/svg/datagrid-select-row-tree.svg +7 -12
  197. package/src/svg/datagrid-select-row.svg +8 -13
  198. package/src/svg/date-type.svg +4 -23
  199. package/src/svg/drawer-tab-hover.svg +4 -6
  200. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  201. package/src/svg/drawer-tab-invert.svg +3 -5
  202. package/src/svg/drawer-tab.svg +5 -5
  203. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  204. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  205. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  206. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  207. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  208. package/src/svg/new-panel-icon.svg +4 -0
  209. package/src/svg/number-type.svg +3 -3
  210. package/src/svg/radio-hover.svg +3 -3
  211. package/src/svg/radio-off.svg +3 -3
  212. package/src/svg/radio-on.svg +3 -3
  213. package/src/svg/status_ok.svg +3 -26
  214. package/src/svg/string-type.svg +4 -8
  215. package/src/svg/theme-icon.svg +5 -5
  216. package/src/themes/blueprint.css +133 -0
  217. package/src/themes/botanical.css +3 -52
  218. package/src/themes/defaults.css +3 -144
  219. package/src/themes/dracula.css +1 -71
  220. package/src/themes/eggplant.css +131 -0
  221. package/src/themes/gruvbox-dark.css +2 -79
  222. package/src/themes/gruvbox.css +2 -40
  223. package/src/themes/icons.css +2 -10
  224. package/src/themes/intl/ar.css +185 -0
  225. package/src/themes/intl/de.css +65 -33
  226. package/src/themes/intl/es.css +60 -28
  227. package/src/themes/intl/fr.css +60 -28
  228. package/src/themes/intl/ja.css +62 -30
  229. package/src/themes/intl/pt.css +58 -26
  230. package/src/themes/intl/zh.css +68 -36
  231. package/src/themes/intl.css +42 -7
  232. package/src/themes/ledger.css +137 -0
  233. package/src/themes/monokai.css +1 -64
  234. package/src/themes/nord.css +131 -0
  235. package/src/themes/phosphor.css +1 -61
  236. package/src/themes/pro-dark.css +1 -52
  237. package/src/themes/pro.css +98 -0
  238. package/src/themes/solarized-dark.css +1 -46
  239. package/src/themes/solarized.css +1 -24
  240. package/src/themes/themes.css +10 -5
  241. package/src/themes/vaporwave.css +1 -57
  242. package/src/themes/velvet.css +132 -0
  243. package/src/ts/column-format.ts +52 -2
  244. package/src/ts/extensions.ts +0 -13
  245. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  246. package/src/css/plugin-settings-panel.css +0 -99
  247. package/src/rust/components/modal.rs +0 -177
  248. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  249. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  250. package/src/rust/custom_elements/modal.rs +0 -299
  251. package/src/svg/dropdown-selector-light.svg +0 -3
  252. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline0.js +0 -0
  253. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline1.js +0 -0
  254. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline2.js +0 -0
  255. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline3.js +0 -0
  256. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline4.js +0 -0
  257. /package/dist/wasm/snippets/{perspective-viewer-39c3c3b28e3cbd48 → perspective-viewer-adc6f3f79141112b}/inline5.js +0 -0
  258. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  259. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  260. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  261. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  262. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  263. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  264. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  265. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -11,7 +11,6 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  mod column_locator;
14
- pub mod drag_helpers;
15
14
  mod props;
16
15
  mod sheets;
17
16
 
@@ -28,13 +27,14 @@ use yew::html::ImplicitClone;
28
27
  use yew::prelude::*;
29
28
 
30
29
  pub use self::column_locator::{
31
- ColumnLocator, ColumnSettingsTab, ColumnSettingsTarget, ColumnTab, OpenColumnSettings,
30
+ ColumnLocator, ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings,
32
31
  };
33
- use self::drag_helpers::DragTargetState;
34
- pub use self::drag_helpers::{DragDropContainer, DragEndCallback};
35
32
  pub use self::props::{DragDropProps, PresentationProps};
36
- use crate::config::{CssKind, NamedValue, assign_palette_names};
37
- use crate::utils::*;
33
+ pub use crate::ui::{DragDropContainer, DragEndCallback};
34
+ use crate::ui::{
35
+ DragTargetState, PointerDownCallback, clear_document_selection, closest_draggable,
36
+ };
37
+ use crate::utils::{CssKind, NamedValue, assign_palette_names, *};
38
38
 
39
39
  #[derive(Clone, Debug)]
40
40
  struct DragFrom {
@@ -73,14 +73,7 @@ pub struct PresentationHandle {
73
73
  pub agent: crate::agent::AgentSlot,
74
74
 
75
75
  /// The available themes as detected in the browser environment or set
76
- /// explicitly when CORS prevents detection — a MEMO of a document
77
- /// external, not component state. `None` until first parsed (detection
78
- /// is expensive and must wait for `document.styleSheets`). Sync-readable
79
- /// (a `RefCell`, never borrowed across an await) so synchronous paths
80
- /// ("stamp with commit" — panel creation, theme mutation sites) can
81
- /// derive the registry default without awaiting registry init; derived
82
- /// values (e.g. the default theme = first registered) are computed from
83
- /// it on demand, never mirrored.
76
+ /// explicitly when CORS prevents detection.
84
77
  themes: RefCell<Option<Vec<String>>>,
85
78
 
86
79
  /// Single-flight guard for the stylesheet parse that populates
@@ -91,12 +84,6 @@ pub struct PresentationHandle {
91
84
 
92
85
  /// Whether the host's theme was ever EXPLICITLY chosen — authored as a
93
86
  /// `theme` attribute, or set by name through [`Self::set_theme_name`].
94
- ///
95
- /// `false` means the host merely displays the registry default, which
96
- /// [`Self::reset_themes`] is free to move; `true` pins the selection
97
- /// until it leaves the registry. The `theme` attribute alone cannot
98
- /// carry this, because `init` stamps it unconditionally so the document
99
- /// cascade has a theme to match.
100
87
  theme_selected: Cell<bool>,
101
88
 
102
89
  palette: RefCell<BTreeMap<String, String>>,
@@ -104,6 +91,8 @@ pub struct PresentationHandle {
104
91
  open_column_settings: RefCell<OpenColumnSettings>,
105
92
  is_workspace: RefCell<Option<bool>>,
106
93
 
94
+ collapsed_control_groups: RefCell<HashSet<String>>,
95
+
107
96
  /// Drag/drop in-progress state. Empty (`NoDrag`) when no user drag is
108
97
  /// active. Mutated by `notify_drag_*` / `notify_drop`; read by component
109
98
  /// CSS-class derivations (`is_dragover`, `get_drag_column`).
@@ -124,32 +113,22 @@ pub struct PresentationHandle {
124
113
  /// dragged element from the shadow tree.
125
114
  host_dragend: RefCell<Option<DragEndCallback>>,
126
115
 
116
+ /// Host-level `pointerdown` listener that clears a stale page selection
117
+ /// before it can turn a row drag into a browser selection drag.
118
+ host_pointerdown: RefCell<Option<PointerDownCallback>>,
119
+
127
120
  source_dragend: RefCell<Option<(web_sys::EventTarget, DragEndCallback)>>,
128
121
 
129
122
  /// IntersectionObserver-based fallback for the drag image, kept alive for
130
123
  /// the duration of the drag.
131
124
  drag_target: RefCell<Option<DragTargetState>>,
132
125
 
133
- /// `(open, announce)` — `announce` says this toggle is the SOLE carrier
134
- /// of the config change (a user gesture: toolbar, `toggleConfig`), so
135
- /// the element must emit `toggle-settings` + a config-update for it. An
136
- /// API restore's toggle is announced by its own view-config commit
137
- /// dispatch instead — announcing here too would double-emit, the first
138
- /// carrying the intermediate config (settings flipped, view fields not
139
- /// yet committed).
140
126
  pub settings_open_changed: PubSub<(bool, bool)>,
141
-
142
- /// Injected callback from the root component, replacing the former
143
- /// `is_workspace_changed: PubSub` field.
144
127
  pub on_is_workspace_changed: RefCell<Option<Callback<bool>>>,
145
128
  pub settings_before_open_changed: PubSub<bool>,
146
129
  pub column_settings_open_changed: PubSub<(bool, Option<String>)>,
147
130
  pub theme_config_updated: PubSub<(PtrEqRc<Vec<String>>, Option<usize>)>,
148
131
  pub on_eject: PubSub<()>,
149
-
150
- /// Fires for status-bar / main-panel pointer events that target the
151
- /// statusbar element. `wire_element_events` formats the `PointerEvent`'s
152
- /// `type_()` into a `perspective-statusbar-{type}` `CustomEvent` name.
153
132
  pub statusbar_pointer_event: PubSub<PointerEvent>,
154
133
  }
155
134
 
@@ -191,6 +170,7 @@ impl Presentation {
191
170
  on_is_workspace_changed: Default::default(),
192
171
  is_settings_open: Default::default(),
193
172
  open_column_settings: Default::default(),
173
+ collapsed_control_groups: Default::default(),
194
174
  theme_config_updated: PubSub::default(),
195
175
  on_eject: PubSub::default(),
196
176
  statusbar_pointer_event: PubSub::default(),
@@ -199,10 +179,12 @@ impl Presentation {
199
179
  on_dragstart: Default::default(),
200
180
  on_dragend: Default::default(),
201
181
  host_dragend: Default::default(),
182
+ host_pointerdown: Default::default(),
202
183
  source_dragend: Default::default(),
203
184
  drag_target: Default::default(),
204
185
  }));
205
186
 
187
+ theme.register_host_pointerdown();
206
188
  ApiFuture::spawn(theme.clone().init());
207
189
  theme
208
190
  }
@@ -357,6 +339,20 @@ impl Presentation {
357
339
  self.open_column_settings.borrow().deref().clone()
358
340
  }
359
341
 
342
+ pub fn is_control_group_collapsed(&self, key: &str) -> bool {
343
+ self.collapsed_control_groups.borrow().contains(key)
344
+ }
345
+
346
+ pub fn set_control_group_collapsed(&self, key: &str, collapsed: bool) {
347
+ if collapsed {
348
+ self.collapsed_control_groups
349
+ .borrow_mut()
350
+ .insert(key.to_owned());
351
+ } else {
352
+ self.collapsed_control_groups.borrow_mut().remove(key);
353
+ }
354
+ }
355
+
360
356
  async fn init(self) -> ApiResult<()> {
361
357
  self.set_theme_attribute(self.get_selected_theme_name().await.as_deref())
362
358
  }
@@ -560,18 +556,53 @@ impl Presentation {
560
556
  }
561
557
  }
562
558
 
563
- pub fn set_drag_image(&self, event: &DragEvent) -> ApiResult<()> {
559
+ /// Claim a `dragstart` for the column drag machinery, returning `false`
560
+ /// (cancelling the native drag) when it did not originate on a
561
+ /// `draggable="true"` row or installation failed.
562
+ pub fn set_drag_image(&self, event: &DragEvent) -> bool {
563
+ match self.try_set_drag_image(event) {
564
+ Ok(true) => true,
565
+ Ok(false) => {
566
+ event.prevent_default();
567
+ if let Err(e) = clear_document_selection() {
568
+ web_sys::console::warn_1(&e.into());
569
+ }
570
+
571
+ false
572
+ },
573
+ Err(e) => {
574
+ event.prevent_default();
575
+ web_sys::console::warn_1(&e.into());
576
+ false
577
+ },
578
+ }
579
+ }
580
+
581
+ fn try_set_drag_image(&self, event: &DragEvent) -> ApiResult<bool> {
564
582
  event.stop_propagation();
583
+ let Some(original) = closest_draggable(event) else {
584
+ return Ok(false);
585
+ };
586
+
587
+ let is_row_drag = event
588
+ .target()
589
+ .and_then(|target| target.dyn_into::<Node>().ok())
590
+ .map(|target| original.is_same_node(Some(&target)))
591
+ .unwrap_or(false);
592
+
593
+ if !is_row_drag {
594
+ return Ok(false);
595
+ }
596
+
565
597
  self.register_source_dragend(event)?;
566
598
  if let Some(dt) = event.data_transfer() {
567
599
  dt.set_drop_effect("move");
568
600
  }
569
601
 
570
- let original: HtmlElement = event.target().into_apierror()?.unchecked_into();
571
602
  let elem: HtmlElement = original
572
603
  .children()
573
604
  .get_with_index(0)
574
- .unwrap()
605
+ .into_apierror()?
575
606
  .clone_node_with_deep(true)?
576
607
  .unchecked_into();
577
608
 
@@ -595,7 +626,7 @@ impl Presentation {
595
626
  Ok(())
596
627
  });
597
628
 
598
- Ok(())
629
+ Ok(true)
599
630
  }
600
631
 
601
632
  /// Is the drag/drop state currently in `action`?
@@ -678,6 +709,22 @@ impl Presentation {
678
709
  Ok(())
679
710
  }
680
711
 
712
+ fn register_host_pointerdown(&self) {
713
+ let closure = Closure::wrap(Box::new(move |event: PointerEvent| {
714
+ if closest_draggable(&event).is_some()
715
+ && let Err(e) = clear_document_selection()
716
+ {
717
+ web_sys::console::warn_1(&e.into());
718
+ }
719
+ }) as Box<dyn FnMut(PointerEvent)>);
720
+
721
+ self.viewer_elem
722
+ .add_event_listener_with_callback("pointerdown", closure.as_ref().unchecked_ref())
723
+ .unwrap();
724
+
725
+ *self.host_pointerdown.borrow_mut() = Some(closure);
726
+ }
727
+
681
728
  fn register_host_dragend(&self) {
682
729
  if let Some(prev) = self.host_dragend.borrow_mut().take() {
683
730
  let _ = self
@@ -0,0 +1,38 @@
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 crate::workspace::Workspace;
14
+
15
+ /// Every hosted `Table` name per loaded `Client`, as `(client name, sorted
16
+ /// table names)` in registration order.
17
+ pub async fn fetch_hosted_tables(workspace: &Workspace) -> Vec<(String, Vec<String>)> {
18
+ let clients = workspace.clients();
19
+ let mut tables = Vec::with_capacity(clients.len());
20
+ for client in clients {
21
+ let mut names = match client.get_hosted_table_names().await {
22
+ Ok(names) => names,
23
+ Err(err) => {
24
+ tracing::warn!(
25
+ "Failed to list tables for `Client` \"{}\": {err}",
26
+ client.get_name()
27
+ );
28
+
29
+ vec![]
30
+ },
31
+ };
32
+
33
+ names.sort_unstable();
34
+ tables.push((client.get_name().to_owned(), names));
35
+ }
36
+
37
+ tables
38
+ }
@@ -28,6 +28,7 @@ pub mod export_app;
28
28
  mod exports;
29
29
  mod fetch_column_stats;
30
30
  mod get_viewer_config;
31
+ mod hosted_tables;
31
32
  mod is_invalid_drop;
32
33
  mod palette_set;
33
34
  mod plugin_column_styles;
@@ -40,6 +41,7 @@ pub use self::export_app::*;
40
41
  pub use self::exports::*;
41
42
  pub use self::fetch_column_stats::*;
42
43
  pub use self::get_viewer_config::*;
44
+ pub use self::hosted_tables::*;
43
45
  pub use self::is_invalid_drop::*;
44
46
  pub use self::palette_set::*;
45
47
  pub use self::plugin_column_styles::*;
@@ -12,8 +12,8 @@
12
12
 
13
13
  use std::collections::BTreeMap;
14
14
 
15
- use crate::config::{CssKind, CssLiteralUse, NamedValue, assign_palette_names};
16
15
  use crate::presentation::Presentation;
16
+ use crate::utils::{CssKind, CssLiteralUse, NamedValue, assign_palette_names};
17
17
  use crate::workspace::{PanelId, Workspace};
18
18
 
19
19
  /// Every CSS literal in use across the workspace, as `(panel id, use)`,
@@ -42,7 +42,7 @@ pub fn get_plugin_config_schema(
42
42
  wasm_bindgen::JsValue::from_serde_ext(view_config).unwrap_or(wasm_bindgen::JsValue::NULL);
43
43
  let raw = plugin._plugin_config_schema(&view_config_js)?;
44
44
  serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
45
- .map(|schema| schema.canonicalize_defaults())
45
+ .map(|schema| schema.canonicalize())
46
46
  .map_err(|e| e.into())
47
47
  }
48
48
 
@@ -100,6 +100,6 @@ pub fn get_column_config_schema(
100
100
  )?;
101
101
 
102
102
  serde_wasm_bindgen::from_value::<ColumnConfigSchema>(raw)
103
- .map(|schema| schema.canonicalize_defaults())
103
+ .map(|schema| schema.canonicalize())
104
104
  .map_err(|e| e.into())
105
105
  }
@@ -51,9 +51,7 @@ pub fn mount_plugin(
51
51
  }
52
52
 
53
53
  /// Given an async `task` which draws `plugin`, [`mount_plugin`] it, then run
54
- /// `task`. Requires the [`RenderGuard`] lock witness — plugin dispatch
55
- /// outside a locked run does not compile (invariants I3/I6 of the merge;
56
- /// see `SESSION_CONFIG_COHERENCE_PLAN.md`).
54
+ /// `task`.
57
55
  pub async fn activate_plugin<T>(
58
56
  _guard: &RenderGuard,
59
57
  viewer: &HtmlElement,
@@ -19,6 +19,7 @@ use std::future::Future;
19
19
  use std::rc::Rc;
20
20
 
21
21
  use perspective_client::View;
22
+ use perspective_client::proto::ViewDimensionsResp;
22
23
  use perspective_js::utils::{ApiResult, ResultTApiErrorExt};
23
24
  use wasm_bindgen::prelude::*;
24
25
  use web_sys::*;
@@ -44,25 +45,27 @@ impl Renderer {
44
45
  let _ = el.class_list().toggle_with_force("multi", !is_solo);
45
46
  }
46
47
 
47
- /// Stamp this panel's effective `theme` attribute.
48
48
  pub fn stamp_theme(&self, plugin: Option<&JsPerspectiveViewerPlugin>) {
49
+ let theme = self.theme();
49
50
  let active_plugin = self.active_plugin();
50
51
  if let Some(plugin) = plugin.or(active_plugin.as_ref()) {
51
- let theme_elem = plugin.unchecked_ref::<HtmlElement>();
52
- match self.theme() {
53
- Some(theme)
54
- if theme_elem.get_attribute("theme").as_deref() != Some(theme.as_str()) =>
55
- {
56
- let _ = theme_elem.set_attribute("theme", &theme);
57
- },
58
- Some(_) => {},
59
- None => {
60
- let _ = theme_elem.remove_attribute("theme");
61
- },
62
- }
52
+ stamp_theme_attr(plugin.unchecked_ref::<Element>(), theme.as_deref());
53
+ }
54
+
55
+ if let Some(tab) = self.tab_host() {
56
+ stamp_theme_attr(&tab, theme.as_deref());
63
57
  }
64
58
  }
65
59
 
60
+ fn tab_host(&self) -> Option<Element> {
61
+ let slot = format!("tab-{}", self.slot_name()?);
62
+ let viewer = self.plugin_data.borrow().viewer_elem.clone();
63
+ let children = viewer.children();
64
+ (0..children.length())
65
+ .filter_map(|i| children.item(i))
66
+ .find(|el| el.get_attribute("slot").as_deref() == Some(slot.as_str()))
67
+ }
68
+
66
69
  /// Restyle the active plugin and re-draw.
67
70
  pub async fn restyle_all(&self) -> ApiResult<JsValue> {
68
71
  self.render_task(|guard| async move {
@@ -78,8 +81,14 @@ impl Renderer {
78
81
  self.stamp_active(&plugin);
79
82
  plugin.restyle();
80
83
  *self.0.captured_theme.borrow_mut() = Some(stamped_theme);
81
- let mut limits =
82
- get_row_and_col_limits(&ctx.view, &meta, self.is_render_warning_enabled()).await?;
84
+ let dimensions = ctx.view.dimensions().await?;
85
+ let mut limits = get_row_and_col_limits(
86
+ &dimensions,
87
+ &ctx.view,
88
+ &meta,
89
+ self.is_render_warning_enabled(),
90
+ )
91
+ .await?;
83
92
 
84
93
  limits.is_update = true;
85
94
  plugin
@@ -165,10 +174,8 @@ impl Renderer {
165
174
  width: f64,
166
175
  height: f64,
167
176
  ) -> ApiResult<Option<js_sys::Function>> {
168
- self.0.presize_pending.set(self.0.presize_pending.get() + 1);
169
- let result = self.presize_inner(None, width, height).await;
170
- self.0.presize_pending.set(self.0.presize_pending.get() - 1);
171
- result
177
+ let _pending = PresizePendingGuard::increment(self);
178
+ self.presize_inner(None, width, height).await
172
179
  }
173
180
 
174
181
  /// Pre-size AND pre-position the plugin to its target layout bo.
@@ -179,10 +186,8 @@ impl Renderer {
179
186
  width: f64,
180
187
  height: f64,
181
188
  ) -> ApiResult<Option<js_sys::Function>> {
182
- self.0.presize_pending.set(self.0.presize_pending.get() + 1);
183
- let result = self.presize_inner(Some((dx, dy)), width, height).await;
184
- self.0.presize_pending.set(self.0.presize_pending.get() - 1);
185
- result
189
+ let _pending = PresizePendingGuard::increment(self);
190
+ self.presize_inner(Some((dx, dy)), width, height).await
186
191
  }
187
192
 
188
193
  async fn presize_inner(
@@ -219,6 +224,7 @@ impl Renderer {
219
224
  };
220
225
 
221
226
  self.stamp_active(&jsplugin);
227
+ self.0.presized_box.set(None);
222
228
  jsplugin.resize().await?;
223
229
  Ok(None)
224
230
  },
@@ -245,7 +251,33 @@ impl Renderer {
245
251
  self.stamp_active(&plugin);
246
252
  self.clear_presize()?;
247
253
  if plugin.capabilities().presize {
248
- return plugin.presize(width, height).await;
254
+ let Some(present) = plugin.presize(width, height).await? else {
255
+ return Ok(None);
256
+ };
257
+
258
+ self.0.presized_box.set(Some((width, height)));
259
+ let inline = js_sys::Reflect::get(&present, &"inline".into())
260
+ .ok()
261
+ .and_then(|v| v.as_bool())
262
+ .unwrap_or(false);
263
+
264
+ if inline {
265
+ self.0.presized_box.set(None);
266
+ }
267
+
268
+ let renderer = self.clone();
269
+ let wrapped = Closure::once_into_js(move || {
270
+ let filled = present
271
+ .call0(&JsValue::NULL)
272
+ .map(|v| v.as_bool() != Some(false))
273
+ .unwrap_or(false);
274
+
275
+ if !filled {
276
+ renderer.0.presized_box.set(None);
277
+ }
278
+ });
279
+
280
+ return Ok(Some(wrapped.unchecked_into::<js_sys::Function>()));
249
281
  }
250
282
 
251
283
  let main_panel: &web_sys::HtmlElement = plugin.unchecked_ref();
@@ -282,6 +314,7 @@ impl Renderer {
282
314
  }
283
315
 
284
316
  result?;
317
+ self.0.presized_box.set(Some((width, height)));
285
318
  }
286
319
 
287
320
  Ok(None)
@@ -311,14 +344,16 @@ impl Renderer {
311
344
  pub async fn draw_fresh(&self, guard: &RenderGuard, view: FreshView) -> ApiResult<()> {
312
345
  let timer = self.render_timer();
313
346
  timer
314
- .capture_time(self.draw_view(guard, view.view(), false))
347
+ .capture_time(self.draw_view(guard, view.view(), false, None))
315
348
  .await
316
349
  }
317
350
 
318
351
  /// Repaint the ALREADY-BOUND `view` on the active plugin.
319
352
  pub async fn update_bound(&self, guard: &RenderGuard, view: &View) -> ApiResult<()> {
320
353
  let timer = self.render_timer();
321
- timer.capture_time(self.draw_view(guard, view, true)).await
354
+ timer
355
+ .capture_time(self.draw_view(guard, view, true, None))
356
+ .await
322
357
  }
323
358
 
324
359
  /// Mint the [`FreshView`] full-draw witness for a plugin that has never
@@ -367,26 +402,33 @@ impl Renderer {
367
402
  /// Redraw an already-bound view, debounced.
368
403
  pub async fn update_lazy(
369
404
  &self,
370
- view: impl Future<Output = ApiResult<Option<View>>>,
405
+ view: impl Future<Output = ApiResult<Option<(View, Option<ViewDimensionsResp>)>>>,
371
406
  ) -> ApiResult<()> {
372
407
  let timer = self.render_timer();
373
408
  self.draw_lock()
374
409
  .debounce_with(|guard| async move {
375
410
  set_timeout(timer.get_throttle()).await?;
376
411
  if self.0.presize_pending.get() > 0 {
412
+ // Record the drop like the hidden-panel path
413
+ // (`wire_panel_render_sub`) does — a silently
414
+ // discarded update otherwise leaves the plugin's
415
+ // retained frame stale until the NEXT data tick,
416
+ // or forever if none comes. `draw_view` /
417
+ // `activation_render` consume the marker.
377
418
  tracing::debug!("Update skipped, presize pending");
419
+ self.set_data_stale();
378
420
  return Ok(());
379
421
  }
380
422
 
381
423
  self.mount_active_plugin()?;
382
- if let Some(view) = view.await? {
424
+ if let Some((view, dimensions)) = view.await? {
383
425
  let _pin = self
384
426
  .cached_context()
385
427
  .map(|ctx| self.pin_context(&guard, ctx));
386
428
 
387
429
  let timer = self.render_timer();
388
430
  timer
389
- .capture_time(self.draw_view(&guard, &view, true))
431
+ .capture_time(self.draw_view(&guard, &view, true, dimensions))
390
432
  .await
391
433
  } else {
392
434
  tracing::debug!("Render skipped, no `View` attached");
@@ -396,11 +438,14 @@ impl Renderer {
396
438
  .await
397
439
  }
398
440
 
441
+ /// `dimensions` is the caller's already-fetched view dimensions, or
442
+ /// `None` to fetch here.
399
443
  async fn draw_view(
400
444
  &self,
401
445
  _guard: &RenderGuard,
402
446
  view: &perspective_client::View,
403
447
  is_update: bool,
448
+ dimensions: Option<ViewDimensionsResp>,
404
449
  ) -> ApiResult<()> {
405
450
  debug_assert!(
406
451
  !is_update || self.cached_context().is_none() || self.render_context().is_some(),
@@ -417,8 +462,14 @@ impl Renderer {
417
462
  };
418
463
 
419
464
  let meta = self.metadata();
465
+ let dimensions = match dimensions {
466
+ Some(dimensions) => dimensions,
467
+ None => view.dimensions().await?,
468
+ };
469
+
420
470
  let mut limits =
421
- get_row_and_col_limits(view, &meta, self.is_render_warning_enabled()).await?;
471
+ get_row_and_col_limits(&dimensions, view, &meta, self.is_render_warning_enabled())
472
+ .await?;
422
473
 
423
474
  limits.is_update = is_update;
424
475
  if let Some(cb) = self.0.on_render_limits_changed.borrow().as_ref() {
@@ -437,6 +488,7 @@ impl Renderer {
437
488
  }
438
489
 
439
490
  self.0.data_stale.set(false);
491
+ self.0.presized_box.set(None);
440
492
  let result = if is_update {
441
493
  let task = plugin.update(view.clone().into(), limits.max_cols, limits.max_rows, false);
442
494
  activate_plugin(_guard, &viewer_elem, &plugin, slot.as_deref(), task).await
@@ -473,3 +525,38 @@ impl Renderer {
473
525
  self.draw_lock.clone()
474
526
  }
475
527
  }
528
+
529
+ /// RAII increment of [`RendererData::presize_pending`], the counter that
530
+ /// gates [`Renderer::update_lazy`] dispatches. Decrementing on `Drop`
531
+ /// (not on the happy path) makes the release unconditional — a presize
532
+ /// future cancelled mid-await can never strand the counter above zero,
533
+ /// which would silently discard every subsequent debounced update for
534
+ /// the panel's lifetime.
535
+ struct PresizePendingGuard(Renderer);
536
+
537
+ impl PresizePendingGuard {
538
+ fn increment(renderer: &Renderer) -> Self {
539
+ let cell = &renderer.0.presize_pending;
540
+ cell.set(cell.get() + 1);
541
+ Self(renderer.clone())
542
+ }
543
+ }
544
+
545
+ impl Drop for PresizePendingGuard {
546
+ fn drop(&mut self) {
547
+ let cell = &self.0.0.presize_pending;
548
+ cell.set(cell.get() - 1);
549
+ }
550
+ }
551
+
552
+ fn stamp_theme_attr(el: &Element, theme: Option<&str>) {
553
+ match theme {
554
+ Some(theme) if el.get_attribute("theme").as_deref() != Some(theme) => {
555
+ let _ = el.set_attribute("theme", theme);
556
+ },
557
+ Some(_) => {},
558
+ None => {
559
+ let _ = el.remove_attribute("theme");
560
+ },
561
+ }
562
+ }
@@ -10,6 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
+ use perspective_client::proto::ViewDimensionsResp;
13
14
  use perspective_js::utils::ApiError;
14
15
 
15
16
  use crate::config::PluginStaticConfig;
@@ -31,16 +32,34 @@ pub struct RenderLimits {
31
32
  pub max_rows: Option<usize>,
32
33
  }
33
34
 
34
- /// Compute the row/column caps for `view` against the plugin's static
35
- /// limits. `render_warning` is the renderer-state flag controlling
36
- /// whether the warning is currently armed; when `false`, an oversized
37
- /// view renders uncapped (the user has dismissed the warning).
35
+ impl RenderLimits {
36
+ /// The column cap is in effect (fewer columns render than the view has).
37
+ pub fn is_col_capped(&self) -> bool {
38
+ self.max_cols.is_some_and(|x| x < self.num_cols)
39
+ }
40
+
41
+ /// The row cap is in effect (fewer rows render than the view has).
42
+ pub fn is_row_capped(&self) -> bool {
43
+ self.max_rows.is_some_and(|x| x < self.num_rows)
44
+ }
45
+
46
+ /// Either cap is in effect — exactly when the render warning shows.
47
+ pub fn is_capped(&self) -> bool {
48
+ self.is_col_capped() || self.is_row_capped()
49
+ }
50
+ }
51
+
52
+ /// Compute the row/column caps for `view` from its already-fetched
53
+ /// `dimensions` against the plugin's static limits. `render_warning` is the
54
+ /// renderer-state flag controlling whether the warning is currently armed;
55
+ /// when `false`, an oversized view renders uncapped (the user has dismissed
56
+ /// the warning).
38
57
  pub async fn get_row_and_col_limits(
58
+ dimensions: &ViewDimensionsResp,
39
59
  view: &perspective_client::View,
40
60
  config: &PluginStaticConfig,
41
61
  render_warning: bool,
42
62
  ) -> Result<RenderLimits, ApiError> {
43
- let dimensions = view.dimensions().await?;
44
63
  let num_cols = dimensions.num_view_columns as usize;
45
64
  let num_rows = dimensions.num_view_rows as usize;
46
65
  match (config.max_columns, render_warning) {