@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
@@ -19,6 +19,10 @@ use wasm_bindgen::prelude::*;
19
19
  use web_sys::*;
20
20
  use yew::prelude::*;
21
21
 
22
+ use crate::components::rows_counter::RowsCounter;
23
+ use crate::session::{Session, ViewStats};
24
+ use crate::utils::Subscription;
25
+
22
26
  #[wasm_bindgen(inline_js = r#"
23
27
  export function define_panel_tab(name) {
24
28
  if (!customElements.get(name)) {
@@ -41,8 +45,6 @@ thread_local! {
41
45
  static ELEMENT_DEFINED: Cell<bool> = const { Cell::new(false) };
42
46
 
43
47
  /// The shared, constructed stylesheet adopted into every tab's ShadowRoot.
44
- /// Built once from `panel-tab.css`; a constructed `CSSStyleSheet` is cheap
45
- /// and safe to adopt into arbitrarily many roots.
46
48
  static TAB_SHEET: CssStyleSheet = {
47
49
  let sheet = CssStyleSheet::new().unwrap();
48
50
  sheet.replace_sync(include_str!("../../css/panel-tab.css"))
@@ -55,11 +57,6 @@ thread_local! {
55
57
  /// lifecycle callbacks — it exists only to own a ShadowRoot into which each
56
58
  /// tab's contents are rendered (so the tab's structure CSS is encapsulated in
57
59
  /// its `adoptedStyleSheets` rather than injected into `document.head`).
58
- ///
59
- /// The host stays a *light-DOM* child of the viewer, so the per-panel `theme`
60
- /// attr is still matched by the document theme rules (`perspective-viewer
61
- /// [theme="X"]`); the `--psp-*` they set are inherited across the shadow
62
- /// boundary into the tab's contents.
63
60
  fn ensure_custom_element() {
64
61
  ELEMENT_DEFINED.with(|defined| {
65
62
  if !defined.get() {
@@ -90,16 +87,6 @@ fn adopt_sheet(shadow_root: &Element) {
90
87
  /// `<regular-layout-frame>` titlebar via a `<slot name="tab-{id}" slot="tab">`
91
88
  /// (rendered by `MainPanel`); the tab's *contents* live in the host's
92
89
  /// **ShadowRoot**, whose `adoptedStyleSheets` carry the structure CSS.
93
- ///
94
- /// Keeping the host in the light DOM — like the plugin — is what lets
95
- /// *document* theme CSS (`perspective-viewer [theme="X"]`) reach it, so each
96
- /// panel's chrome is themed by the native cascade instead of a runtime-inlined
97
- /// string; the `--psp-*` those rules set are inherited across the shadow
98
- /// boundary into the contents. The host carries `part="tab"`, which
99
- /// `<regular-layout-frame>`'s pointer handler treats as a drag handle
100
- /// (regular-layout@>=0.6.0's custom-tab contract); the title is
101
- /// `pointer-events:none` so clicks fall through to the host (like the built-in
102
- /// `<regular-layout-tab>`).
103
90
  #[derive(Properties, PartialEq)]
104
91
  pub struct PanelTabProps {
105
92
  /// The `<perspective-viewer>` host element; the tab is attached here as a
@@ -112,6 +99,10 @@ pub struct PanelTabProps {
112
99
  /// The panel's title; falls back to the id when `None`.
113
100
  pub title: Option<String>,
114
101
 
102
+ /// This panel's own `Session`, whose `stats_changed` the tab subscribes
103
+ /// to directly so a stats tick re-renders only this tab.
104
+ pub session: Session,
105
+
115
106
  /// This panel's effective theme. Reflected onto the host's `theme`
116
107
  /// attribute so the document theme rules (`perspective-viewer [theme="X"]`)
117
108
  /// theme the tab per-panel via the native cascade.
@@ -147,6 +138,11 @@ pub struct PanelTabProps {
147
138
  /// at zero panels).
148
139
  pub is_settings_open: bool,
149
140
 
141
+ /// `true` while this panel's close awaits its layout commit, which keeps
142
+ /// the tab rendered but inert.
143
+ #[prop_or_default]
144
+ pub closing: bool,
145
+
150
146
  /// `false` for a lone panel — suppresses the tab rearrange-drag.
151
147
  /// `<regular-layout-frame>` arms a drag from any `part="tab"` pointerdown,
152
148
  /// but a lone panel has nowhere to drop, so the host `pointerdown` handler
@@ -189,56 +185,81 @@ const TITLE_PLACEHOLDER: &str = "untitled";
189
185
 
190
186
  pub enum PanelTabMsg {
191
187
  /// A pointerdown on the tab host. Selects the panel; and when it's the
192
- /// second within [`DBLCLICK_MS`], enters title-edit mode. Carries the
193
- /// event `timeStamp` (ms). We synthesize the double-click from pointerdown
194
- /// because `<regular-layout-frame>` calls `setPointerCapture` +
195
- /// `preventDefault` on the tab's pointerdown (to arm a drag), which
196
- /// suppresses the browser's synthesized `click`/`dblclick` — so a native
197
- /// `dblclick` listener never fires on the tab.
188
+ /// second within [`DBLCLICK_MS`], enters title-edit mode.
198
189
  PointerDown(f64),
199
190
  Close,
191
+
200
192
  /// The tab's open-settings button (shown while the settings sidebar is
201
193
  /// closed, in the close button's place).
202
194
  OpenSettings,
203
195
  ContextMenu(f64, f64),
196
+
204
197
  /// Track the live `<input>` value while editing (drives the auto-sizer).
205
198
  EditInput(String),
199
+
206
200
  /// Commit the edited title to the panel's session (blur / Enter).
207
201
  CommitEdit,
202
+
208
203
  /// Abandon the edit, restoring the previous title (Escape).
209
204
  CancelEdit,
205
+
206
+ /// This panel's `Session::stats_changed` fired; re-read the stats.
207
+ StatsChanged,
210
208
  }
211
209
 
212
210
  pub struct PanelTab {
213
211
  host: HtmlElement,
214
- /// The host's open ShadowRoot; the tab's contents are portaled here (stored
215
- /// as `Element` for `create_portal`, matching `PortalModal`).
216
212
  shadow_root: Element,
217
- /// `pointerdown` listener on the host (kept alive); selects this panel and
218
- /// synthesizes double-click-to-edit (see [`PanelTabMsg::PointerDown`]).
219
213
  _pointerdown: Closure<dyn FnMut(PointerEvent)>,
220
- /// `contextmenu` listener on the host (kept alive); opens the panel menu.
221
214
  _contextmenu: Closure<dyn FnMut(MouseEvent)>,
222
- /// Current `draggable` prop, shared into the `pointerdown` closure so a
223
- /// prop change (lone ⇄ multi panel) takes effect without re-creating it.
224
215
  draggable: Rc<Cell<bool>>,
216
+
225
217
  /// `timeStamp` (ms) of the last host pointerdown, for synthesizing
226
- /// double-clicks. `NEG_INFINITY` until the first pointerdown.
218
+ /// double-clicks.
227
219
  last_pointerdown: f64,
220
+
228
221
  /// `true` while the title is being edited (renders an `<input>`).
229
222
  editing: bool,
223
+
230
224
  /// Live edited value; controls the `<input>` and the auto-sizer width.
231
225
  edit_value: String,
226
+
232
227
  /// The edit `<input>`, for focusing on edit entry.
233
228
  input_ref: NodeRef,
229
+
234
230
  /// Focus + select-all the input on the next render after entering edit
235
231
  /// mode.
236
232
  focus_pending: bool,
233
+
234
+ /// The `theme` prop value last written to the host.
235
+ stamped_theme: Option<Option<String>>,
236
+
237
+ /// This panel's `Table`/`View` dimensions, rendered as the `RowsCounter`
238
+ /// after the title.
239
+ stats: Option<ViewStats>,
240
+
241
+ /// Subscription to `session.stats_changed`, armed only while the tab is
242
+ /// `visible`.
243
+ _stats_sub: Option<Subscription>,
237
244
  }
238
245
 
239
246
  impl PanelTab {
240
247
  fn slot_name(panel_id: &str) -> String {
241
- format!("tab-{panel_id}")
248
+ format!("tab-{}", panel_id)
249
+ }
250
+
251
+ /// Arm the `session.stats_changed` subscription and take a fresh stats
252
+ /// snapshot.
253
+ fn arm_stats(&mut self, ctx: &Context<Self>) {
254
+ let cb = ctx.link().callback(|_: ()| PanelTabMsg::StatsChanged);
255
+ self._stats_sub = Some(ctx.props().session.stats_changed.add_notify_listener(&cb));
256
+ self.stats = ctx.props().session.get_table_stats();
257
+ }
258
+
259
+ /// Drop the stats subscription and snapshot.
260
+ fn disarm_stats(&mut self) {
261
+ self._stats_sub = None;
262
+ self.stats = None;
242
263
  }
243
264
 
244
265
  /// Enter title-edit mode, seeding the input with the *real* title (empty
@@ -261,8 +282,8 @@ impl PanelTab {
261
282
  /// tab of every stack, not just the single active panel; `single`/`multi`
262
283
  /// reflect the host viewer's panel count. The shadow CSS keys off
263
284
  /// `:host(.active)` / `:host(.visible)` / `:host(.single)`.
264
- fn sync_class(&self, active: bool, visible: bool, single: bool) {
265
- let mut class = Vec::with_capacity(3);
285
+ fn sync_class(&self, active: bool, visible: bool, single: bool, closing: bool) {
286
+ let mut class = Vec::with_capacity(4);
266
287
  if active {
267
288
  class.push("active");
268
289
  }
@@ -271,6 +292,10 @@ impl PanelTab {
271
292
  class.push("visible");
272
293
  }
273
294
 
295
+ if closing {
296
+ class.push("closing");
297
+ }
298
+
274
299
  class.push(if single { "single" } else { "multi" });
275
300
  let _ = self.host.set_attribute("class", &class.join(" "));
276
301
  }
@@ -331,7 +356,7 @@ impl Component for PanelTab {
331
356
  let _ = host
332
357
  .add_event_listener_with_callback("contextmenu", contextmenu.as_ref().unchecked_ref());
333
358
 
334
- Self {
359
+ let mut tab = Self {
335
360
  host,
336
361
  shadow_root,
337
362
  _pointerdown: pointerdown,
@@ -342,7 +367,16 @@ impl Component for PanelTab {
342
367
  edit_value: String::new(),
343
368
  input_ref: NodeRef::default(),
344
369
  focus_pending: false,
370
+ stamped_theme: None,
371
+ stats: None,
372
+ _stats_sub: None,
373
+ };
374
+
375
+ if ctx.props().visible {
376
+ tab.arm_stats(ctx);
345
377
  }
378
+
379
+ tab
346
380
  }
347
381
 
348
382
  fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
@@ -352,6 +386,12 @@ impl Component for PanelTab {
352
386
  .set_attribute("slot", &Self::slot_name(&ctx.props().panel_id));
353
387
  }
354
388
 
389
+ if !ctx.props().visible {
390
+ self.disarm_stats();
391
+ } else if self._stats_sub.is_none() || ctx.props().session != old.session {
392
+ self.arm_stats(ctx);
393
+ }
394
+
355
395
  self.draggable.set(ctx.props().draggable);
356
396
  true
357
397
  }
@@ -400,6 +440,12 @@ impl Component for PanelTab {
400
440
  self.editing = false;
401
441
  true
402
442
  },
443
+ PanelTabMsg::StatsChanged => {
444
+ let stats = ctx.props().session.get_table_stats();
445
+ let changed = stats != self.stats;
446
+ self.stats = stats;
447
+ changed
448
+ },
403
449
  }
404
450
  }
405
451
 
@@ -465,6 +511,7 @@ impl Component for PanelTab {
465
511
  <span class="psp-tab-caret" />
466
512
  // <span class="psp-tab-grip" />
467
513
  { title_html }
514
+ if ctx.props().visible { <RowsCounter stats={self.stats.clone()} /> }
468
515
  if ctx.props().is_master { <span class="psp-tab-master" /> }
469
516
  if !ctx.props().is_settings_open {
470
517
  <button class="psp-tab-settings" onpointerdown={on_open_settings} />
@@ -478,14 +525,24 @@ impl Component for PanelTab {
478
525
  }
479
526
 
480
527
  fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
481
- self.sync_class(ctx.props().active, ctx.props().visible, ctx.props().single);
482
- match &ctx.props().theme {
483
- Some(theme) => {
484
- let _ = self.host.set_attribute("theme", theme);
485
- },
486
- None => {
487
- let _ = self.host.remove_attribute("theme");
488
- },
528
+ self.sync_class(
529
+ ctx.props().active,
530
+ ctx.props().visible,
531
+ ctx.props().single,
532
+ ctx.props().closing,
533
+ );
534
+ let theme = &ctx.props().theme;
535
+ if self.stamped_theme.as_ref() != Some(theme) {
536
+ match theme {
537
+ Some(theme) => {
538
+ let _ = self.host.set_attribute("theme", theme);
539
+ },
540
+ None => {
541
+ let _ = self.host.remove_attribute("theme");
542
+ },
543
+ }
544
+
545
+ self.stamped_theme = Some(theme.clone());
489
546
  }
490
547
 
491
548
  if !self.host.is_connected() {
@@ -23,10 +23,12 @@ use crate::components::column_settings_sidebar::style_tab::primitive_field::{
23
23
  BoolField, ColorField, EnumField, NumberFieldPrimitive,
24
24
  };
25
25
  use crate::config::ControlSpec;
26
+ use crate::presentation::Presentation;
26
27
  use crate::queries::get_plugin_config_schema;
27
28
  use crate::renderer::Renderer;
28
29
  use crate::session::Session;
29
30
  use crate::tasks::send_plugin_config;
31
+ use crate::ui::ControlGroup;
30
32
  use crate::utils::PtrEqRc;
31
33
 
32
34
  #[derive(Clone, PartialEq, Properties)]
@@ -47,6 +49,7 @@ pub struct PluginTabProps {
47
49
  pub plugin_config: PtrEqRc<serde_json::Map<String, serde_json::Value>>,
48
50
 
49
51
  // State
52
+ pub presentation: Presentation,
50
53
  pub renderer: Renderer,
51
54
  pub session: Session,
52
55
  }
@@ -95,98 +98,133 @@ pub fn PluginTab(props: &PluginTabProps) -> Html {
95
98
  })
96
99
  };
97
100
 
98
- let raw_config = &*props.plugin_config;
99
- let components = schema
100
- .iter()
101
- .cloned()
102
- .filter_map(|spec| {
103
- let component = match spec {
104
- ControlSpec::Enum {
105
- key,
106
- variants,
107
- default,
108
- } => {
109
- let current = raw_config
110
- .get(&key)
111
- .and_then(|v| v.as_str().map(|s| s.to_string()));
112
- html! {
113
- <EnumField
114
- field_key={key}
115
- {variants}
116
- {default}
117
- {current}
118
- on_change={on_change.clone()}
119
- />
120
- }
121
- },
122
- ControlSpec::Bool { key, default } => {
123
- let current = raw_config.get(&key).and_then(|v| v.as_bool());
124
- html! {
125
- <BoolField
126
- field_key={key}
127
- {default}
128
- {current}
129
- on_change={on_change.clone()}
130
- />
131
- }
132
- },
133
- ControlSpec::Color { key, default } => {
134
- let current = raw_config
135
- .get(&key)
136
- .and_then(|v| v.as_str().map(|s| s.to_string()));
137
- html! {
138
- <ColorField
139
- field_key={key}
140
- {default}
141
- {current}
142
- on_change={on_change.clone()}
143
- />
144
- }
145
- },
146
- ControlSpec::Number {
147
- key,
148
- default,
149
- min,
150
- max,
151
- step,
152
- include,
153
- } => {
154
- let current = raw_config.get(&key).and_then(|v| v.as_f64());
155
- html! {
156
- <NumberFieldPrimitive
157
- field_key={key}
158
- {default}
159
- {current}
160
- {min}
161
- {max}
162
- {step}
163
- {include}
164
- on_change={on_change.clone()}
165
- />
166
- }
167
- },
168
- // Column-scoped variants don't apply to
169
- // plugin-level config; drop silently.
170
- ControlSpec::AggregateDepth
171
- | ControlSpec::NumberSeriesStyle { .. }
172
- | ControlSpec::DatetimeFormat
173
- | ControlSpec::StringFormat
174
- | ControlSpec::Symbols { .. }
175
- | ControlSpec::NumberFormat
176
- | ControlSpec::String { .. }
177
- | ControlSpec::Palette { .. }
178
- | ControlSpec::GradientStops { .. } => {
179
- return None;
180
- },
181
- };
182
-
183
- Some(html! { <fieldset class="style-control">{ component }</fieldset> })
101
+ let on_group_toggle = {
102
+ let presentation = props.presentation.clone();
103
+ yew::Callback::from(move |(key, open): (String, bool)| {
104
+ presentation.set_control_group_collapsed(&key, !open);
184
105
  })
185
- .collect_vec();
106
+ };
186
107
 
108
+ let raw_config = &*props.plugin_config;
109
+ let components = render_specs(
110
+ &schema,
111
+ raw_config,
112
+ &on_change,
113
+ &props.presentation,
114
+ &on_group_toggle,
115
+ );
187
116
  html! {
188
117
  <div id="plugin-tab" class="sidebar_column scrollable">
189
118
  <div id="plugin-config-container" class="tab-section">{ components }</div>
190
119
  </div>
191
120
  }
192
121
  }
122
+
123
+ fn render_specs(
124
+ specs: &[ControlSpec],
125
+ raw_config: &serde_json::Map<String, serde_json::Value>,
126
+ on_change: &Callback<crate::config::ColumnConfigFieldUpdate>,
127
+ presentation: &Presentation,
128
+ on_group_toggle: &Callback<(String, bool)>,
129
+ ) -> Vec<Html> {
130
+ specs
131
+ .iter()
132
+ .filter_map(|spec| match spec {
133
+ ControlSpec::Group { key, fields } => {
134
+ let children =
135
+ render_specs(fields, raw_config, on_change, presentation, on_group_toggle);
136
+
137
+ (!children.is_empty()).then(|| {
138
+ html! {
139
+ <ControlGroup
140
+ key={format!("group::{key}")}
141
+ group_key={key.clone()}
142
+ open={!presentation.is_control_group_collapsed(key)}
143
+ on_toggle={on_group_toggle.clone()}
144
+ >
145
+ { children }
146
+ </ControlGroup>
147
+ }
148
+ })
149
+ },
150
+ leaf => {
151
+ let key = leaf.serialized_keys().join("+");
152
+ let component = render_leaf(leaf, raw_config, on_change)?;
153
+ Some(html! { <fieldset class="style-control" {key}>{ component }</fieldset> })
154
+ },
155
+ })
156
+ .collect_vec()
157
+ }
158
+
159
+ fn render_leaf(
160
+ spec: &ControlSpec,
161
+ raw_config: &serde_json::Map<String, serde_json::Value>,
162
+ on_change: &Callback<crate::config::ColumnConfigFieldUpdate>,
163
+ ) -> Option<Html> {
164
+ match spec.clone() {
165
+ ControlSpec::Enum {
166
+ key,
167
+ variants,
168
+ default,
169
+ } => {
170
+ let current = raw_config
171
+ .get(&key)
172
+ .and_then(|v| v.as_str().map(|s| s.to_string()));
173
+ Some(html! {
174
+ <EnumField
175
+ field_key={key}
176
+ {variants}
177
+ {default}
178
+ {current}
179
+ on_change={on_change.clone()}
180
+ />
181
+ })
182
+ },
183
+ ControlSpec::Bool { key, default } => {
184
+ let current = raw_config.get(&key).and_then(|v| v.as_bool());
185
+ Some(html! {
186
+ <BoolField field_key={key} {default} {current} on_change={on_change.clone()} />
187
+ })
188
+ },
189
+ ControlSpec::Color { key, default } => {
190
+ let current = raw_config
191
+ .get(&key)
192
+ .and_then(|v| v.as_str().map(|s| s.to_string()));
193
+ Some(html! {
194
+ <ColorField field_key={key} {default} {current} on_change={on_change.clone()} />
195
+ })
196
+ },
197
+ ControlSpec::Number {
198
+ key,
199
+ default,
200
+ min,
201
+ max,
202
+ step,
203
+ include,
204
+ } => {
205
+ let current = raw_config.get(&key).and_then(|v| v.as_f64());
206
+ Some(html! {
207
+ <NumberFieldPrimitive
208
+ field_key={key}
209
+ {default}
210
+ {current}
211
+ {min}
212
+ {max}
213
+ {step}
214
+ {include}
215
+ on_change={on_change.clone()}
216
+ />
217
+ })
218
+ },
219
+ ControlSpec::Group { .. }
220
+ | ControlSpec::AggregateDepth
221
+ | ControlSpec::NumberSeriesStyle { .. }
222
+ | ControlSpec::DatetimeFormat { .. }
223
+ | ControlSpec::StringFormat
224
+ | ControlSpec::Symbols { .. }
225
+ | ControlSpec::NumberFormat { .. }
226
+ | ControlSpec::String { .. }
227
+ | ControlSpec::Palette { .. }
228
+ | ControlSpec::GradientStops { .. } => None,
229
+ }
230
+ }
@@ -27,13 +27,13 @@ pub struct RenderWarningProps {
27
27
  pub fn render_warning(props: &RenderWarningProps) -> Html {
28
28
  let dimensions = props.dimensions;
29
29
  let (col_warn, row_warn) = if let Some(limits) = dimensions {
30
- let col_warn = if limits.max_cols.is_some_and(|x| x < limits.num_cols) {
30
+ let col_warn = if limits.is_col_capped() {
31
31
  Some((limits.max_cols.unwrap(), limits.num_cols))
32
32
  } else {
33
33
  None
34
34
  };
35
35
 
36
- let row_warn = if limits.max_rows.is_some_and(|x| x < limits.num_rows) {
36
+ let row_warn = if limits.is_row_capped() {
37
37
  Some((
38
38
  limits.num_cols * limits.max_rows.unwrap(),
39
39
  limits.num_cols * limits.num_rows,
@@ -15,17 +15,17 @@ use yew::prelude::*;
15
15
  use crate::session::ViewStats;
16
16
  use crate::utils::u32Ext;
17
17
 
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).
18
+ /// Pure value props: no `Session` handle, no PubSub subscriptions. The parent
19
+ /// (`PanelTab`) passes its panel's `stats` snapshot, refreshed by the root on
20
+ /// that panel's `Session::stats_changed`.
21
21
  #[derive(PartialEq, Properties)]
22
- pub struct StatusBarRowsCounterProps {
22
+ pub struct RowsCounterProps {
23
23
  pub stats: Option<ViewStats>,
24
24
  }
25
25
 
26
- /// A component to show the current [`Table`]'s dimensions.
26
+ /// Shows a panel's `Table`/`View` dimensions in its `PanelTab` header.
27
27
  #[function_component]
28
- pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
28
+ pub fn RowsCounter(props: &RowsCounterProps) -> Html {
29
29
  match props.stats {
30
30
  Some(
31
31
  ViewStats {
@@ -49,7 +49,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
49
49
  <span id="rows">
50
50
  <span>{ vrows }</span>
51
51
  <span class="total">{ format!(" ({})", nrows) }</span>
52
- <span class="x">{ " x " }</span>
52
+ <span class="x">{ " / " }</span>
53
53
  <span>{ vcols }</span>
54
54
  <span class="total">{ format!(" ({})", ncols) }</span>
55
55
  </span>
@@ -77,7 +77,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
77
77
  <span id="rows">
78
78
  <span>{ vrows }</span>
79
79
  <span class="total">{ format!(" ({})", nrows) }</span>
80
- <span class="x">{ " x " }</span>
80
+ <span class="x">{ " / " }</span>
81
81
  <span>{ vcols }</span>
82
82
  </span>
83
83
  }
@@ -94,7 +94,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
94
94
  html! {
95
95
  <span id="rows">
96
96
  <span>{ vrows }</span>
97
- <span class="x">{ " x " }</span>
97
+ <span class="x">{ " / " }</span>
98
98
  <span>{ vcols }</span>
99
99
  <span class="total">{ format!(" ({})", ncols) }</span>
100
100
  </span>
@@ -110,7 +110,7 @@ pub fn StatusBarRowsCounter(props: &StatusBarRowsCounterProps) -> Html {
110
110
  html! {
111
111
  <span id="rows">
112
112
  <span>{ nrows }</span>
113
- <span class="x">{ " x " }</span>
113
+ <span class="x">{ " / " }</span>
114
114
  <span>{ ncols }</span>
115
115
  </span>
116
116
  }
@@ -19,8 +19,7 @@ use yew::prelude::*;
19
19
  use super::column_selector::ColumnSelector;
20
20
  use super::plugin_selector::PluginSelector;
21
21
  use super::plugin_tab::PluginTab;
22
- use crate::components::containers::sidebar_close_button::SidebarCloseButton;
23
- use crate::components::form::debug::DebugPanel;
22
+ use crate::components::debug_panel::DebugPanel;
24
23
  use crate::config::{PluginStaticConfig, PluginUpdate};
25
24
  use crate::presentation::{ColumnLocator, ColumnSettingsTarget, OpenColumnSettings, Presentation};
26
25
  use crate::queries::classify_column;
@@ -28,6 +27,7 @@ use crate::renderer::*;
28
27
  use crate::session::column_defaults_update::*;
29
28
  use crate::session::*;
30
29
  use crate::tasks::update_plugin_and_render;
30
+ use crate::ui::SidebarCloseButton;
31
31
  use crate::utils::*;
32
32
  use crate::workspace::Workspace;
33
33
 
@@ -370,6 +370,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
370
370
  <PluginTab
371
371
  view_config={props.view_config.clone()}
372
372
  plugin_config={props.plugin_config.clone()}
373
+ presentation={presentation.clone()}
373
374
  renderer={renderer.clone()}
374
375
  session={session.clone()}
375
376
  // initial_width={width}