@perspective-dev/viewer 4.5.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/esm/extensions.d.ts +22 -3
  17. package/dist/esm/perspective-viewer.d.ts +9 -0
  18. package/dist/esm/perspective-viewer.inline.js +20 -1
  19. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  20. package/dist/esm/perspective-viewer.js +20 -1
  21. package/dist/esm/perspective-viewer.js.map +4 -4
  22. package/dist/esm/plugin.d.ts +71 -5
  23. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  24. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  25. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  26. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  27. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  28. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  29. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  30. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  32. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  33. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  34. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  35. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  36. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  37. package/dist/wasm/perspective-viewer.d.ts +211 -77
  38. package/dist/wasm/perspective-viewer.js +528 -180
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  41. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  42. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  43. package/package.json +3 -2
  44. package/src/css/_context_menu.css +14 -0
  45. package/src/css/column-settings-panel.css +1 -1
  46. package/src/css/containers/context-menu.css +101 -0
  47. package/src/css/panel-tab.css +193 -0
  48. package/src/css/plugin-selector.css +7 -10
  49. package/src/css/status-bar.css +75 -81
  50. package/src/css/viewer.css +180 -70
  51. package/src/rust/components/column_dropdown.rs +40 -30
  52. package/src/rust/components/column_selector/active_column.rs +7 -18
  53. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  54. package/src/rust/components/column_selector/config_selector.rs +33 -81
  55. package/src/rust/components/column_selector/filter_column.rs +5 -15
  56. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  57. package/src/rust/components/column_selector/sort_column.rs +3 -6
  58. package/src/rust/components/column_selector.rs +5 -11
  59. package/src/rust/components/containers/split_panel.rs +175 -62
  60. package/src/rust/components/context_menu.rs +130 -0
  61. package/src/rust/components/form/debug.rs +3 -0
  62. package/src/rust/components/global_filter_bar.rs +76 -0
  63. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  64. package/src/rust/components/main_panel/msg.rs +62 -0
  65. package/src/rust/components/main_panel/presize.rs +220 -0
  66. package/src/rust/components/main_panel/reconcile.rs +224 -0
  67. package/src/rust/components/main_panel/render.rs +385 -0
  68. package/src/rust/components/main_panel/update.rs +184 -0
  69. package/src/rust/components/main_panel.rs +311 -71
  70. package/src/rust/components/mod.rs +4 -0
  71. package/src/rust/components/panel_menu.rs +486 -0
  72. package/src/rust/components/panel_tab.rs +505 -0
  73. package/src/rust/components/plugin_tab.rs +1 -1
  74. package/src/rust/components/settings_panel.rs +13 -5
  75. package/src/rust/components/status_bar.rs +110 -178
  76. package/src/rust/components/status_indicator.rs +6 -9
  77. package/src/rust/components/style/mod.rs +5 -0
  78. package/src/rust/components/style/style_provider.rs +2 -0
  79. package/src/rust/components/viewer/filters.rs +133 -0
  80. package/src/rust/components/viewer/msg.rs +180 -0
  81. package/src/rust/components/viewer/panels.rs +326 -0
  82. package/src/rust/components/viewer/render.rs +243 -0
  83. package/src/rust/components/viewer/settings.rs +402 -0
  84. package/src/rust/components/viewer/snapshots.rs +134 -0
  85. package/src/rust/components/viewer/wiring.rs +505 -0
  86. package/src/rust/components/viewer.rs +243 -756
  87. package/src/rust/config/export_method.rs +1 -1
  88. package/src/rust/config/mod.rs +4 -0
  89. package/src/rust/config/options.rs +71 -0
  90. package/src/rust/config/viewer_config.rs +24 -2
  91. package/src/rust/config/workspace_config.rs +92 -0
  92. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  93. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  94. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  95. package/src/rust/custom_elements/viewer.rs +1015 -286
  96. package/src/rust/custom_events.rs +212 -61
  97. package/src/rust/js/mod.rs +2 -0
  98. package/src/rust/js/plugin.rs +297 -10
  99. package/src/rust/js/regular_layout.rs +258 -0
  100. package/src/rust/lib.rs +20 -1
  101. package/src/rust/presentation.rs +66 -33
  102. package/src/rust/queries/get_viewer_config.rs +29 -11
  103. package/src/rust/queries/plugin_column_styles.rs +2 -2
  104. package/src/rust/renderer/activate.rs +41 -14
  105. package/src/rust/renderer/dispatch.rs +436 -0
  106. package/src/rust/renderer/plugin_config.rs +495 -0
  107. package/src/rust/renderer/registry.rs +18 -6
  108. package/src/rust/renderer/render_timer.rs +29 -10
  109. package/src/rust/renderer.rs +350 -837
  110. package/src/rust/session/column_defaults_update.rs +26 -8
  111. package/src/rust/session/view_subscription.rs +30 -15
  112. package/src/rust/session.rs +753 -264
  113. package/src/rust/tasks/apply_global_filters.rs +89 -0
  114. package/src/rust/tasks/auto_pause.rs +171 -0
  115. package/src/rust/tasks/copy_export.rs +29 -19
  116. package/src/rust/tasks/create_panel.rs +243 -0
  117. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  118. package/src/rust/tasks/edit_expression.rs +4 -4
  119. package/src/rust/tasks/eject.rs +64 -23
  120. package/src/rust/tasks/mod.rs +28 -4
  121. package/src/rust/tasks/pipeline.rs +411 -0
  122. package/src/rust/tasks/presize_panels.rs +324 -0
  123. package/src/rust/tasks/reset_all.rs +22 -12
  124. package/src/rust/tasks/resize_observer.rs +124 -87
  125. package/src/rust/tasks/restore_and_render.rs +80 -86
  126. package/src/rust/tasks/restore_panel.rs +145 -0
  127. package/src/rust/tasks/send_column_config.rs +5 -2
  128. package/src/rust/tasks/send_plugin_config.rs +5 -2
  129. package/src/rust/tasks/set_edit_mode.rs +59 -0
  130. package/src/rust/tasks/sync_update_panels.rs +152 -0
  131. package/src/rust/tasks/table_lifecycle.rs +158 -0
  132. package/src/rust/tasks/update_theme.rs +56 -12
  133. package/src/rust/utils/completion.rs +56 -0
  134. package/src/rust/utils/custom_element.rs +5 -0
  135. package/src/rust/utils/debounce.rs +130 -7
  136. package/src/rust/utils/mod.rs +6 -0
  137. package/src/rust/utils/pending_effects.rs +62 -0
  138. package/src/rust/utils/spawn.rs +33 -0
  139. package/src/rust/utils/weak_scope.rs +0 -10
  140. package/src/rust/workspace.rs +846 -0
  141. package/src/svg/active-indicator.svg +3 -0
  142. package/src/themes/botanical.css +5 -2
  143. package/src/themes/defaults.css +54 -2
  144. package/src/themes/dracula.css +6 -3
  145. package/src/themes/gruvbox-dark.css +5 -2
  146. package/src/themes/gruvbox.css +5 -2
  147. package/src/themes/icons.css +4 -2
  148. package/src/themes/monokai.css +5 -2
  149. package/src/themes/phosphor.css +5 -2
  150. package/src/themes/pro-dark.css +5 -2
  151. package/src/themes/pro.css +2 -1
  152. package/src/themes/solarized-dark.css +5 -2
  153. package/src/themes/solarized.css +5 -2
  154. package/src/themes/vaporwave.css +5 -2
  155. package/src/ts/bootstrap.ts +8 -7
  156. package/src/ts/extensions.ts +30 -2
  157. package/src/ts/perspective-viewer.ts +10 -0
  158. package/src/ts/plugin.ts +72 -5
  159. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  160. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  161. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  162. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  163. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  164. package/src/ts/ts-rs/Layout.ts +9 -0
  165. package/src/ts/ts-rs/Orientation.ts +6 -0
  166. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  168. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  169. package/src/ts/ts-rs/TabLayout.ts +11 -0
  170. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  171. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  172. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  173. package/src/rust/tasks/intersection_observer.rs +0 -98
  174. package/src/rust/tasks/update_and_render.rs +0 -110
  175. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -0,0 +1,505 @@
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
+ #[wasm_bindgen(inline_js = r#"
23
+ export function define_panel_tab(name) {
24
+ if (!customElements.get(name)) {
25
+ customElements.define(name, class extends HTMLElement {});
26
+ }
27
+ }
28
+ "#)]
29
+ extern "C" {
30
+ #[wasm_bindgen(js_name = "define_panel_tab")]
31
+ fn define_panel_tab(name: &str);
32
+ }
33
+
34
+ /// The tag the tab host is created as (a custom element, so it can own a
35
+ /// ShadowRoot for its contents).
36
+ const TAB_TAG: &str = "perspective-viewer-tab";
37
+
38
+ thread_local! {
39
+ /// Whether the `<perspective-viewer-tab>` custom element has been defined
40
+ /// (once per page; WASM is single-threaded).
41
+ static ELEMENT_DEFINED: Cell<bool> = const { Cell::new(false) };
42
+
43
+ /// 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
+ static TAB_SHEET: CssStyleSheet = {
47
+ let sheet = CssStyleSheet::new().unwrap();
48
+ sheet.replace_sync(include_str!("../../css/panel-tab.css"))
49
+ .unwrap();
50
+ sheet
51
+ };
52
+ }
53
+
54
+ /// Define the `<perspective-viewer-tab>` custom element once. It needs no
55
+ /// lifecycle callbacks — it exists only to own a ShadowRoot into which each
56
+ /// tab's contents are rendered (so the tab's structure CSS is encapsulated in
57
+ /// 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
+ fn ensure_custom_element() {
64
+ ELEMENT_DEFINED.with(|defined| {
65
+ if !defined.get() {
66
+ define_panel_tab(TAB_TAG);
67
+ defined.set(true);
68
+ }
69
+ });
70
+ }
71
+
72
+ /// Adopt the shared tab stylesheet into `shadow_root.adoptedStyleSheets`
73
+ /// (idempotent per root). Mirrors `StyleProvider`'s adopt-by-`Reflect`
74
+ /// approach.
75
+ fn adopt_sheet(shadow_root: &Element) {
76
+ let sheets = js_sys::Reflect::get(shadow_root.as_ref(), &"adoptedStyleSheets".into())
77
+ .unwrap()
78
+ .unchecked_into::<js_sys::Array>();
79
+
80
+ TAB_SHEET.with(|sheet| {
81
+ let sheet_val: &JsValue = sheet.as_ref();
82
+ if sheets.index_of(sheet_val, 0) < 0 {
83
+ sheets.push(sheet_val);
84
+ }
85
+ });
86
+ }
87
+
88
+ /// A panel's titlebar tab. The host is a `<perspective-viewer-tab>` custom
89
+ /// element mounted in the viewer's **light DOM** and forwarded into its
90
+ /// `<regular-layout-frame>` titlebar via a `<slot name="tab-{id}" slot="tab">`
91
+ /// (rendered by `MainPanel`); the tab's *contents* live in the host's
92
+ /// **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
+ #[derive(Properties, PartialEq)]
104
+ pub struct PanelTabProps {
105
+ /// The `<perspective-viewer>` host element; the tab is attached here as a
106
+ /// light-DOM child (mirrors `renderer::activate`'s plugin mount).
107
+ pub viewer: HtmlElement,
108
+
109
+ /// The panel id. The tab is assigned to `slot="tab-{panel_id}"`.
110
+ pub panel_id: String,
111
+
112
+ /// The panel's title; falls back to the id when `None`.
113
+ pub title: Option<String>,
114
+
115
+ /// This panel's effective theme. Reflected onto the host's `theme`
116
+ /// attribute so the document theme rules (`perspective-viewer [theme="X"]`)
117
+ /// theme the tab per-panel via the native cascade.
118
+ pub theme: Option<String>,
119
+
120
+ /// `true` when this is the active panel (toolbar target / selected tab);
121
+ /// drives the active-tab styling.
122
+ pub active: bool,
123
+
124
+ /// `true` when this panel is *visible* — the front (selected) tab of its
125
+ /// stack, or a lone panel. Hidden panels are those at an unselected index
126
+ /// of a tab stack. Independent of `active`: every stack has a visible
127
+ /// panel, but only one panel in the whole layout is active.
128
+ pub visible: bool,
129
+
130
+ /// `true` when this panel is a master (filter-source) panel — shows the
131
+ /// broadcast badge to the left of the close button.
132
+ pub is_master: bool,
133
+
134
+ /// `true` when this is the host viewer's ONLY panel (one plugin child).
135
+ /// Reflected on the host as a `single`/`multi` class — the same
136
+ /// panel-count CSS hook `Renderer::stamp_active` stamps on the plugin —
137
+ /// e.g. the caret is hidden on a lone tab (see panel-tab.css).
138
+ pub single: bool,
139
+
140
+ /// `false` for a lone panel (which can't be closed to zero) — hides the
141
+ /// close button.
142
+ pub closable: bool,
143
+
144
+ /// `true` when the settings sidebar is open. When closed, the tab renders
145
+ /// an open-settings button *in place of* the close button — the only
146
+ /// affordance for opening the settings panel (there is deliberately none
147
+ /// at zero panels).
148
+ pub is_settings_open: bool,
149
+
150
+ /// `false` for a lone panel — suppresses the tab rearrange-drag.
151
+ /// `<regular-layout-frame>` arms a drag from any `part="tab"` pointerdown,
152
+ /// but a lone panel has nowhere to drop, so the host `pointerdown` handler
153
+ /// stops the event before it reaches the frame (see `create`).
154
+ pub draggable: bool,
155
+
156
+ /// Select this panel in the layout (brings its frame forward within a stack
157
+ /// and activates it). Wired by `MainPanel` to `RegularLayout::select`.
158
+ pub on_select: Callback<String>,
159
+
160
+ /// Remove this panel from the layout. Wired by `MainPanel` to the root
161
+ /// `ClosePanel` message (which mutates the `Workspace` model first, then
162
+ /// syncs the slave `regular-layout` — NOT `RegularLayout::remove_panel`
163
+ /// directly; see the app-initiated-layout-change invariant).
164
+ pub on_close: Callback<String>,
165
+
166
+ /// Open the settings sidebar targeting this panel. Wired by `MainPanel` to
167
+ /// select this panel in the layout, activate it (so the sidebar binds its
168
+ /// engines), then toggle the sidebar open.
169
+ pub on_open_settings: Callback<String>,
170
+
171
+ /// Open the panel context menu at `(client_x, client_y)`. Wired here on the
172
+ /// tab host because the tab's content is a `create_portal` subtree, so its
173
+ /// events don't reach the frame's main-tree `oncontextmenu` (unlike the
174
+ /// imperatively-mounted plugin body).
175
+ pub on_context_menu: Callback<(String, f64, f64)>,
176
+
177
+ /// Commit a new title for this panel. Wired by `MainPanel` to this panel's
178
+ /// own [`Session::set_title`](crate::session::Session::set_title).
179
+ /// `(panel_id, new_title)`; `None` clears it back to the id fallback.
180
+ pub on_rename: Callback<(String, Option<String>)>,
181
+ }
182
+
183
+ /// Max gap (ms) between two tab pointerdowns to count as a double-click.
184
+ const DBLCLICK_MS: f64 = 400.0;
185
+
186
+ /// Shown in place of the title when a panel has no explicit title (rather than
187
+ /// falling back to the table / plugin name).
188
+ const TITLE_PLACEHOLDER: &str = "untitled";
189
+
190
+ pub enum PanelTabMsg {
191
+ /// 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.
198
+ PointerDown(f64),
199
+ Close,
200
+ /// The tab's open-settings button (shown while the settings sidebar is
201
+ /// closed, in the close button's place).
202
+ OpenSettings,
203
+ ContextMenu(f64, f64),
204
+ /// Track the live `<input>` value while editing (drives the auto-sizer).
205
+ EditInput(String),
206
+ /// Commit the edited title to the panel's session (blur / Enter).
207
+ CommitEdit,
208
+ /// Abandon the edit, restoring the previous title (Escape).
209
+ CancelEdit,
210
+ }
211
+
212
+ pub struct PanelTab {
213
+ 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
+ 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
+ _pointerdown: Closure<dyn FnMut(PointerEvent)>,
220
+ /// `contextmenu` listener on the host (kept alive); opens the panel menu.
221
+ _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
+ draggable: Rc<Cell<bool>>,
225
+ /// `timeStamp` (ms) of the last host pointerdown, for synthesizing
226
+ /// double-clicks. `NEG_INFINITY` until the first pointerdown.
227
+ last_pointerdown: f64,
228
+ /// `true` while the title is being edited (renders an `<input>`).
229
+ editing: bool,
230
+ /// Live edited value; controls the `<input>` and the auto-sizer width.
231
+ edit_value: String,
232
+ /// The edit `<input>`, for focusing on edit entry.
233
+ input_ref: NodeRef,
234
+ /// Focus + select-all the input on the next render after entering edit
235
+ /// mode.
236
+ focus_pending: bool,
237
+ }
238
+
239
+ impl PanelTab {
240
+ fn slot_name(panel_id: &str) -> String {
241
+ format!("tab-{panel_id}")
242
+ }
243
+
244
+ /// Enter title-edit mode, seeding the input with the *real* title (empty
245
+ /// when `None`, not the id fallback shown in display mode). Ignores
246
+ /// re-entry so a click while editing doesn't clobber the in-progress
247
+ /// edit. Returns whether a re-render is needed.
248
+ fn begin_edit(&mut self, ctx: &Context<Self>) -> bool {
249
+ if self.editing {
250
+ return false;
251
+ }
252
+
253
+ self.editing = true;
254
+ self.focus_pending = true;
255
+ self.edit_value = ctx.props().title.clone().unwrap_or_default();
256
+ true
257
+ }
258
+
259
+ /// Reflect `active`/`visible`/`single` onto the host (not a vnode — so
260
+ /// its class is set imperatively). `visible` marks the front (selected)
261
+ /// tab of every stack, not just the single active panel; `single`/`multi`
262
+ /// reflect the host viewer's panel count. The shadow CSS keys off
263
+ /// `: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);
266
+ if active {
267
+ class.push("active");
268
+ }
269
+
270
+ if visible {
271
+ class.push("visible");
272
+ }
273
+
274
+ class.push(if single { "single" } else { "multi" });
275
+ let _ = self.host.set_attribute("class", &class.join(" "));
276
+ }
277
+ }
278
+
279
+ impl Component for PanelTab {
280
+ type Message = PanelTabMsg;
281
+ type Properties = PanelTabProps;
282
+
283
+ fn create(ctx: &Context<Self>) -> Self {
284
+ ensure_custom_element();
285
+
286
+ let host: HtmlElement = global::document()
287
+ .create_element(TAB_TAG)
288
+ .unwrap()
289
+ .unchecked_into();
290
+
291
+ host.set_attribute("part", "tab").unwrap();
292
+ host.set_attribute("slot", &Self::slot_name(&ctx.props().panel_id))
293
+ .unwrap();
294
+
295
+ let init = ShadowRootInit::new(ShadowRootMode::Open);
296
+ let shadow_root = host
297
+ .shadow_root()
298
+ .unwrap_or_else(|| host.attach_shadow(&init).unwrap())
299
+ .unchecked_into::<Element>();
300
+
301
+ adopt_sheet(&shadow_root);
302
+ let link = ctx.link().clone();
303
+ let draggable = Rc::new(Cell::new(ctx.props().draggable));
304
+ let drag_flag = draggable.clone();
305
+ let pointerdown = Closure::wrap(Box::new(move |event: PointerEvent| {
306
+ if !drag_flag.get() {
307
+ event.stop_propagation();
308
+ event.prevent_default();
309
+ }
310
+
311
+ let ts = event.unchecked_ref::<Event>().time_stamp();
312
+ link.send_message(PanelTabMsg::PointerDown(ts));
313
+ }) as Box<dyn FnMut(PointerEvent)>);
314
+ let _ = host
315
+ .add_event_listener_with_callback("pointerdown", pointerdown.as_ref().unchecked_ref());
316
+
317
+ let link = ctx.link().clone();
318
+ let contextmenu = Closure::wrap(Box::new(move |event: MouseEvent| {
319
+ event.prevent_default();
320
+ event.stop_propagation();
321
+ link.send_message(PanelTabMsg::ContextMenu(
322
+ event.client_x() as f64,
323
+ event.client_y() as f64,
324
+ ));
325
+ }) as Box<dyn FnMut(MouseEvent)>);
326
+ let _ = host
327
+ .add_event_listener_with_callback("contextmenu", contextmenu.as_ref().unchecked_ref());
328
+
329
+ Self {
330
+ host,
331
+ shadow_root,
332
+ _pointerdown: pointerdown,
333
+ _contextmenu: contextmenu,
334
+ draggable,
335
+ last_pointerdown: f64::NEG_INFINITY,
336
+ editing: false,
337
+ edit_value: String::new(),
338
+ input_ref: NodeRef::default(),
339
+ focus_pending: false,
340
+ }
341
+ }
342
+
343
+ fn changed(&mut self, ctx: &Context<Self>, old: &Self::Properties) -> bool {
344
+ if ctx.props().panel_id != old.panel_id {
345
+ let _ = self
346
+ .host
347
+ .set_attribute("slot", &Self::slot_name(&ctx.props().panel_id));
348
+ }
349
+
350
+ self.draggable.set(ctx.props().draggable);
351
+ true
352
+ }
353
+
354
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
355
+ let id = ctx.props().panel_id.clone();
356
+ match msg {
357
+ PanelTabMsg::PointerDown(ts) => {
358
+ ctx.props().on_select.emit(id);
359
+ let is_double = ts - self.last_pointerdown <= DBLCLICK_MS;
360
+ self.last_pointerdown = ts;
361
+ is_double && self.begin_edit(ctx)
362
+ },
363
+ PanelTabMsg::Close => {
364
+ ctx.props().on_close.emit(id);
365
+ false
366
+ },
367
+ PanelTabMsg::OpenSettings => {
368
+ ctx.props().on_open_settings.emit(id);
369
+ false
370
+ },
371
+ PanelTabMsg::ContextMenu(x, y) => {
372
+ ctx.props().on_context_menu.emit((id, x, y));
373
+ false
374
+ },
375
+ PanelTabMsg::EditInput(value) => {
376
+ self.edit_value = value;
377
+ true
378
+ },
379
+ PanelTabMsg::CommitEdit => {
380
+ if !self.editing {
381
+ return false;
382
+ }
383
+
384
+ self.editing = false;
385
+ let value = self.edit_value.trim();
386
+ let title = (!value.is_empty()).then(|| value.to_owned());
387
+ ctx.props().on_rename.emit((id, title));
388
+ true
389
+ },
390
+ PanelTabMsg::CancelEdit => {
391
+ if !self.editing {
392
+ return false;
393
+ }
394
+
395
+ self.editing = false;
396
+ true
397
+ },
398
+ }
399
+ }
400
+
401
+ fn view(&self, ctx: &Context<Self>) -> Html {
402
+ let on_close = ctx.link().callback(|e: PointerEvent| {
403
+ e.stop_propagation();
404
+ PanelTabMsg::Close
405
+ });
406
+
407
+ let on_open_settings = ctx.link().callback(|e: PointerEvent| {
408
+ e.stop_propagation();
409
+ PanelTabMsg::OpenSettings
410
+ });
411
+
412
+ let title_html = if self.editing {
413
+ let oninput = ctx.link().callback(|e: InputEvent| {
414
+ let value = e
415
+ .target()
416
+ .map(|t| t.unchecked_into::<HtmlInputElement>().value())
417
+ .unwrap_or_default();
418
+ PanelTabMsg::EditInput(value)
419
+ });
420
+ let onblur = ctx.link().callback(|_: FocusEvent| PanelTabMsg::CommitEdit);
421
+ let onkeydown = ctx
422
+ .link()
423
+ .batch_callback(|e: KeyboardEvent| match e.key().as_str() {
424
+ "Enter" => vec![PanelTabMsg::CommitEdit],
425
+ "Escape" => vec![PanelTabMsg::CancelEdit],
426
+ _ => vec![],
427
+ });
428
+
429
+ let onpointerdown = ctx.link().batch_callback(|e: PointerEvent| {
430
+ e.stop_propagation();
431
+ Vec::<PanelTabMsg>::new()
432
+ });
433
+
434
+ html! {
435
+ <label class="psp-tab-title input-sizer" data-value={self.edit_value.clone()}>
436
+ <input
437
+ ref={self.input_ref.clone()}
438
+ value={self.edit_value.clone()}
439
+ {oninput}
440
+ {onblur}
441
+ {onkeydown}
442
+ {onpointerdown}
443
+ />
444
+ </label>
445
+ }
446
+ } else {
447
+ // The panel's explicit title, else a placeholder (NOT the table /
448
+ // plugin name) rendered in the inactive color (see panel-tab.css
449
+ // `.psp-tab-title.placeholder`).
450
+ match ctx.props().title.clone().filter(|t| !t.is_empty()) {
451
+ Some(title) => html! { <span class="psp-tab-title">{ title }</span> },
452
+ None => html! {
453
+ <span class="psp-tab-title placeholder">{ TITLE_PLACEHOLDER }</span>
454
+ },
455
+ }
456
+ };
457
+
458
+ let content = html! {
459
+ <>
460
+ <span class="psp-tab-caret" />
461
+ // <span class="psp-tab-grip" />
462
+ { title_html }
463
+ if ctx.props().is_master { <span class="psp-tab-master" /> }
464
+ if !ctx.props().is_settings_open {
465
+ <button class="psp-tab-settings" onpointerdown={on_open_settings} />
466
+ } else if ctx.props().closable {
467
+ <button class="psp-tab-close" onpointerdown={on_close} />
468
+ }
469
+ </>
470
+ };
471
+
472
+ yew::create_portal(content, self.shadow_root.clone())
473
+ }
474
+
475
+ fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
476
+ self.sync_class(ctx.props().active, ctx.props().visible, ctx.props().single);
477
+ match &ctx.props().theme {
478
+ Some(theme) => {
479
+ let _ = self.host.set_attribute("theme", theme);
480
+ },
481
+ None => {
482
+ let _ = self.host.remove_attribute("theme");
483
+ },
484
+ }
485
+
486
+ if !self.host.is_connected() {
487
+ let _ = ctx.props().viewer.append_child(&self.host);
488
+ }
489
+
490
+ // Focus + select-all once, on entry to edit mode.
491
+ if self.focus_pending {
492
+ self.focus_pending = false;
493
+ if let Some(input) = self.input_ref.cast::<HtmlInputElement>() {
494
+ let _ = input.focus();
495
+ input.select();
496
+ }
497
+ }
498
+ }
499
+
500
+ fn destroy(&mut self, ctx: &Context<Self>) {
501
+ if self.host.is_connected() {
502
+ let _ = ctx.props().viewer.remove_child(&self.host);
503
+ }
504
+ }
505
+ }
@@ -87,7 +87,7 @@ pub fn PluginTab(props: &PluginTabProps) -> Html {
87
87
  yew::Callback::from(move |update: crate::config::ColumnConfigFieldUpdate| {
88
88
  // `send_plugin_config` emits `plugin_config_changed`,
89
89
  // which the root component's subscription
90
- // (`create_subscriptions`) turns into an `UpdateRenderer`
90
+ // (`create_active_subscriptions`) turns into an `UpdateRenderer`
91
91
  // dispatch carrying a fresh `RendererProps`. Yew's prop
92
92
  // diff propagates the new `plugin_config` into this
93
93
  // component automatically — no manual revision bump.
@@ -26,7 +26,7 @@ use crate::presentation::{ColumnLocator, OpenColumnSettings, Presentation};
26
26
  use crate::renderer::*;
27
27
  use crate::session::column_defaults_update::*;
28
28
  use crate::session::*;
29
- use crate::tasks::update_and_render;
29
+ use crate::tasks::update_plugin_and_render;
30
30
  use crate::utils::*;
31
31
 
32
32
  #[derive(Clone, Properties)]
@@ -161,7 +161,7 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
161
161
  let on_auto_width = props.on_auto_width.clone();
162
162
 
163
163
  // Dispatch callback: captures engine handles, constructs config update,
164
- // hands the apply+draw work to `tasks::update_and_render`.
164
+ // hands the apply+draw work to `tasks::pipeline`.
165
165
  let on_select_plugin = {
166
166
  clone!(renderer, session, presentation);
167
167
  let session_metadata = props.metadata.clone();
@@ -170,9 +170,16 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
170
170
  if session.is_errored() {
171
171
  return;
172
172
  }
173
- let metadata = renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name));
173
+ // Pure resolve — the swap itself is committed inside the locked
174
+ // draw task by `update_plugin_and_render`, never staged on the
175
+ // `Renderer` where a concurrent draw could observe it.
176
+ let resolved_plugin =
177
+ renderer.resolve_plugin_update(&PluginUpdate::Update(plugin_name));
174
178
  let prev_metadata = renderer.metadata();
175
- let plugin_config = metadata.as_deref().unwrap_or(&*prev_metadata);
179
+ let plugin_config = resolved_plugin
180
+ .as_ref()
181
+ .map(|(_, metadata)| &**metadata)
182
+ .unwrap_or(&*prev_metadata);
176
183
  let rollup_features = session_metadata
177
184
  .get_features()
178
185
  .map(|x| x.get_group_rollup_modes())
@@ -190,7 +197,8 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html {
190
197
  plugin_config,
191
198
  );
192
199
 
193
- if let Ok(task) = update_and_render(&session, &renderer, update) {
200
+ let plugin_idx = resolved_plugin.map(|(idx, _)| idx);
201
+ if let Ok(task) = update_plugin_and_render(&session, &renderer, update, plugin_idx) {
194
202
  ApiFuture::spawn(task);
195
203
  }
196
204