@perspective-dev/viewer 4.5.2 → 5.1.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.
- package/dist/cdn/perspective-viewer.js +20 -1
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/esm/extensions.d.ts +22 -3
- package/dist/esm/perspective-viewer.d.ts +9 -0
- package/dist/esm/perspective-viewer.inline.js +20 -1
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +20 -1
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +71 -5
- package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
- package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
- package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
- package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
- package/dist/esm/ts-rs/Layout.d.ts +11 -0
- package/dist/esm/ts-rs/Orientation.d.ts +4 -0
- package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +41 -0
- package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
- package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +8 -2
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +9 -0
- package/dist/esm/ts-rs/WindowAggregate.d.ts +1 -0
- package/dist/esm/ts-rs/WindowSort.d.ts +7 -0
- package/dist/esm/ts-rs/WindowSortDir.d.ts +6 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +46 -0
- package/dist/esm/ts-rs/Windows.d.ts +10 -0
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
- package/dist/wasm/perspective-viewer.d.ts +299 -78
- package/dist/wasm/perspective-viewer.js +544 -186
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-8a8c7d5d696b43ae/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-8a8c7d5d696b43ae/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-selector.css +1 -0
- package/src/css/column-settings-panel.css +68 -3
- package/src/css/column-style.css +4 -3
- package/src/css/config-selector.css +22 -1
- package/src/css/containers/context-menu.css +107 -0
- package/src/css/containers/tabs.css +2 -2
- package/src/css/panel-tab.css +193 -0
- package/src/css/plugin-selector.css +7 -10
- package/src/css/status-bar.css +75 -81
- package/src/css/viewer.css +188 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +28 -45
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/column_selector_column_row.rs +73 -0
- package/src/rust/components/column_selector/config_selector.rs +40 -82
- package/src/rust/components/column_selector/expr_edit_button.rs +7 -1
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +11 -9
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +25 -14
- package/src/rust/components/column_settings_sidebar/style_tab.rs +2 -1
- package/src/rust/components/column_settings_sidebar/window_tab.rs +34 -0
- package/src/rust/components/column_settings_sidebar.rs +163 -16
- package/src/rust/components/containers/dragdrop_list.rs +73 -3
- package/src/rust/components/containers/sidebar.rs +99 -22
- package/src/rust/components/containers/split_panel.rs +181 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/editable_header.rs +2 -0
- package/src/rust/components/form/debug.rs +3 -0
- package/src/rust/components/global_filter_bar.rs +76 -0
- package/src/rust/components/main_panel/frame_theme.rs +186 -0
- package/src/rust/components/main_panel/msg.rs +62 -0
- package/src/rust/components/main_panel/presize.rs +220 -0
- package/src/rust/components/main_panel/reconcile.rs +224 -0
- package/src/rust/components/main_panel/render.rs +385 -0
- package/src/rust/components/main_panel/update.rs +184 -0
- package/src/rust/components/main_panel.rs +316 -71
- package/src/rust/components/mod.rs +5 -0
- package/src/rust/components/panel_menu.rs +493 -0
- package/src/rust/components/panel_tab.rs +510 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/portal.rs +7 -0
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +110 -178
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/style/mod.rs +5 -0
- package/src/rust/components/style/style_provider.rs +2 -0
- package/src/rust/components/viewer/filters.rs +133 -0
- package/src/rust/components/viewer/msg.rs +182 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +255 -0
- package/src/rust/components/viewer/settings.rs +434 -0
- package/src/rust/components/viewer/snapshots.rs +134 -0
- package/src/rust/components/viewer/wiring.rs +505 -0
- package/src/rust/components/viewer.rs +245 -756
- package/src/rust/components/window_editor.rs +1013 -0
- package/src/rust/config/export_method.rs +1 -1
- package/src/rust/config/mod.rs +4 -0
- package/src/rust/config/options.rs +71 -0
- package/src/rust/config/viewer_config.rs +24 -2
- package/src/rust/config/workspace_config.rs +92 -0
- package/src/rust/custom_elements/copy_dropdown.rs +3 -1
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +3 -1
- package/src/rust/custom_elements/viewer.rs +1015 -286
- package/src/rust/custom_events.rs +212 -61
- package/src/rust/js/mod.rs +2 -0
- package/src/rust/js/plugin.rs +297 -10
- package/src/rust/js/regular_layout.rs +258 -0
- package/src/rust/lib.rs +24 -2
- package/src/rust/presentation/column_locator.rs +16 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/columns_iter_set.rs +7 -1
- package/src/rust/queries/get_viewer_config.rs +29 -11
- package/src/rust/queries/plugin_column_styles.rs +2 -2
- package/src/rust/renderer/activate.rs +41 -14
- package/src/rust/renderer/dispatch.rs +463 -0
- package/src/rust/renderer/plugin_config.rs +495 -0
- package/src/rust/renderer/registry.rs +18 -6
- package/src/rust/renderer/render_timer.rs +29 -10
- package/src/rust/renderer.rs +384 -838
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/drag_drop_update.rs +18 -0
- package/src/rust/session/metadata.rs +66 -9
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +755 -264
- package/src/rust/tasks/apply_global_filters.rs +89 -0
- package/src/rust/tasks/auto_pause.rs +171 -0
- package/src/rust/tasks/copy_export.rs +29 -19
- package/src/rust/tasks/create_panel.rs +243 -0
- package/src/rust/tasks/dismiss_render_warning.rs +3 -1
- package/src/rust/tasks/edit_expression.rs +4 -4
- package/src/rust/tasks/edit_window.rs +155 -0
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +30 -4
- package/src/rust/tasks/pipeline.rs +411 -0
- package/src/rust/tasks/presize_panels.rs +324 -0
- package/src/rust/tasks/reset_all.rs +22 -12
- package/src/rust/tasks/resize_observer.rs +124 -87
- package/src/rust/tasks/restore_and_render.rs +80 -86
- package/src/rust/tasks/restore_panel.rs +145 -0
- package/src/rust/tasks/send_column_config.rs +5 -2
- package/src/rust/tasks/send_plugin_config.rs +5 -2
- package/src/rust/tasks/set_edit_mode.rs +59 -0
- package/src/rust/tasks/sync_update_panels.rs +152 -0
- package/src/rust/tasks/table_lifecycle.rs +158 -0
- package/src/rust/tasks/update_theme.rs +56 -12
- package/src/rust/utils/browser/dragdrop.rs +12 -0
- package/src/rust/utils/browser/tests/debounce.rs +49 -0
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +189 -17
- package/src/rust/utils/mod.rs +6 -0
- package/src/rust/utils/pending_effects.rs +62 -0
- package/src/rust/utils/spawn.rs +33 -0
- package/src/rust/utils/weak_scope.rs +0 -10
- package/src/rust/workspace.rs +846 -0
- package/src/svg/active-indicator.svg +3 -0
- package/src/svg/pin-icon.svg +5 -0
- package/src/themes/botanical.css +5 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +6 -3
- package/src/themes/gruvbox-dark.css +5 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +5 -2
- package/src/themes/intl/de.css +7 -0
- package/src/themes/intl/es.css +7 -0
- package/src/themes/intl/fr.css +7 -0
- package/src/themes/intl/ja.css +7 -0
- package/src/themes/intl/pt.css +7 -0
- package/src/themes/intl/zh.css +7 -0
- package/src/themes/intl.css +10 -1
- package/src/themes/monokai.css +5 -2
- package/src/themes/phosphor.css +5 -2
- package/src/themes/pro-dark.css +5 -2
- package/src/themes/pro.css +2 -1
- package/src/themes/solarized-dark.css +5 -2
- package/src/themes/solarized.css +5 -2
- package/src/themes/vaporwave.css +5 -2
- package/src/ts/bootstrap.ts +8 -7
- package/src/ts/extensions.ts +30 -2
- package/src/ts/perspective-viewer.ts +10 -0
- package/src/ts/plugin.ts +72 -5
- package/src/ts/ts-rs/ClientOptions.ts +7 -0
- package/src/ts/ts-rs/ExportMethod.ts +3 -0
- package/src/ts/ts-rs/ExportOptions.ts +8 -0
- package/src/ts/ts-rs/GetClientOptions.ts +7 -0
- package/src/ts/ts-rs/GetTableOptions.ts +7 -0
- package/src/ts/ts-rs/Layout.ts +9 -0
- package/src/ts/ts-rs/Orientation.ts +6 -0
- package/src/ts/ts-rs/PanelOptions.ts +7 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +16 -0
- package/src/ts/ts-rs/SplitLayout.ts +5 -0
- package/src/ts/ts-rs/TabLayout.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +7 -2
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +9 -0
- package/src/ts/ts-rs/WindowAggregate.ts +3 -0
- package/src/ts/ts-rs/WindowSort.ts +9 -0
- package/src/ts/ts-rs/WindowSortDir.ts +8 -0
- package/src/ts/ts-rs/WindowSpec.ts +46 -0
- package/src/ts/ts-rs/Windows.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
- package/src/rust/tasks/intersection_observer.rs +0 -98
- package/src/rust/tasks/update_and_render.rs +0 -110
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-8a8c7d5d696b43ae/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-8a8c7d5d696b43ae/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-8a8c7d5d696b43ae/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-8a8c7d5d696b43ae/inline5.js} +0 -0
|
@@ -0,0 +1,846 @@
|
|
|
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
|
+
//! The multi-panel model backing a single `<perspective-viewer>`.
|
|
14
|
+
|
|
15
|
+
use std::cell::RefCell;
|
|
16
|
+
use std::collections::HashSet;
|
|
17
|
+
use std::rc::Rc;
|
|
18
|
+
|
|
19
|
+
use perspective_client::Client;
|
|
20
|
+
use perspective_client::config::Filter;
|
|
21
|
+
|
|
22
|
+
use crate::renderer::Renderer;
|
|
23
|
+
use crate::session::Session;
|
|
24
|
+
use crate::utils::{EffectLedger, PubSub, Subscription};
|
|
25
|
+
|
|
26
|
+
/// A unique identifier for a [`Panel`] within a [`Workspace`].
|
|
27
|
+
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
28
|
+
pub struct PanelId(String);
|
|
29
|
+
|
|
30
|
+
impl PanelId {
|
|
31
|
+
pub fn as_str(&self) -> &str {
|
|
32
|
+
&self.0
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl std::fmt::Display for PanelId {
|
|
37
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
38
|
+
f.write_str(&self.0)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl From<String> for PanelId {
|
|
43
|
+
fn from(value: String) -> Self {
|
|
44
|
+
Self(value)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl From<&str> for PanelId {
|
|
49
|
+
fn from(value: &str) -> Self {
|
|
50
|
+
Self(value.to_owned())
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// The element-level global filter state (master/detail cross-filter): an
|
|
55
|
+
/// unattributed `restored` bucket (from whole-element `restore` — per-master
|
|
56
|
+
/// attribution is not persisted) plus one contribution per master panel, in
|
|
57
|
+
/// first-contribution order. The effective set is the ordered, deduped
|
|
58
|
+
/// flattening of both. Pure data — factored out of [`WorkspaceData`] so the
|
|
59
|
+
/// replace/dedup/removal semantics are unit-testable without engine handles.
|
|
60
|
+
#[derive(Default)]
|
|
61
|
+
struct GlobalFilterSet {
|
|
62
|
+
restored: Vec<Filter>,
|
|
63
|
+
contributions: Vec<(PanelId, Vec<Filter>)>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
impl GlobalFilterSet {
|
|
67
|
+
/// The effective set: `restored`, then each contribution in order,
|
|
68
|
+
/// deduped by clause equality.
|
|
69
|
+
fn flatten(&self) -> Vec<Filter> {
|
|
70
|
+
let mut flat: Vec<Filter> = Vec::new();
|
|
71
|
+
let all = self
|
|
72
|
+
.restored
|
|
73
|
+
.iter()
|
|
74
|
+
.chain(self.contributions.iter().flat_map(|(_, fs)| fs.iter()));
|
|
75
|
+
|
|
76
|
+
for filter in all {
|
|
77
|
+
if !flat.contains(filter) {
|
|
78
|
+
flat.push(filter.clone());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
flat
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Apply `f`, reporting whether the EFFECTIVE (flattened) set changed —
|
|
86
|
+
/// internal-only moves (e.g. a clause migrating buckets) don't count, so
|
|
87
|
+
/// observers re-render/re-query only on visible change.
|
|
88
|
+
fn with_change(&mut self, f: impl FnOnce(&mut Self)) -> bool {
|
|
89
|
+
let before = self.flatten();
|
|
90
|
+
f(self);
|
|
91
|
+
before != self.flatten()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Replace `id`'s contribution (a master's new selection REPLACES its
|
|
95
|
+
/// prior one — no plugin-remembered remove-lists). A non-empty selection
|
|
96
|
+
/// also drops the `restored` bucket, which is a stale snapshot of some
|
|
97
|
+
/// pre-save master's selection. Empty removes the entry (deselect).
|
|
98
|
+
fn set_contribution(&mut self, id: &PanelId, filters: Vec<Filter>) -> bool {
|
|
99
|
+
self.with_change(|s| {
|
|
100
|
+
if filters.is_empty() {
|
|
101
|
+
s.contributions.retain(|(pid, _)| pid != id);
|
|
102
|
+
} else {
|
|
103
|
+
s.restored.clear();
|
|
104
|
+
match s.contributions.iter_mut().find(|(pid, _)| pid == id) {
|
|
105
|
+
Some((_, fs)) => *fs = filters,
|
|
106
|
+
None => s.contributions.push((id.clone(), filters)),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Remove the flattened-view clause at `index` from EVERY bucket (a chip
|
|
113
|
+
/// stands for the clause, not one bucket's copy — removing it from only
|
|
114
|
+
/// one would resurface the duplicate). Returns whether the effective set
|
|
115
|
+
/// changed and the OWNING master panels of the removed clause (for
|
|
116
|
+
/// selection-state cleanup). Out-of-range is a no-op.
|
|
117
|
+
fn remove_clause(&mut self, index: usize) -> (bool, Vec<PanelId>) {
|
|
118
|
+
let Some(clause) = self.flatten().get(index).cloned() else {
|
|
119
|
+
return (false, Vec::new());
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let owners = self
|
|
123
|
+
.contributions
|
|
124
|
+
.iter()
|
|
125
|
+
.filter(|(_, fs)| fs.contains(&clause))
|
|
126
|
+
.map(|(pid, _)| pid.clone())
|
|
127
|
+
.collect();
|
|
128
|
+
|
|
129
|
+
let changed = self.with_change(|s| {
|
|
130
|
+
s.restored.retain(|f| f != &clause);
|
|
131
|
+
for (_, fs) in s.contributions.iter_mut() {
|
|
132
|
+
fs.retain(|f| f != &clause);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
s.contributions.retain(|(_, fs)| !fs.is_empty());
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
(changed, owners)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Drop both buckets, returning the contribution owners (for
|
|
142
|
+
/// selection-state cleanup).
|
|
143
|
+
fn clear(&mut self) -> (bool, Vec<PanelId>) {
|
|
144
|
+
let owners = self
|
|
145
|
+
.contributions
|
|
146
|
+
.iter()
|
|
147
|
+
.map(|(pid, _)| pid.clone())
|
|
148
|
+
.collect();
|
|
149
|
+
|
|
150
|
+
let changed = self.with_change(|s| {
|
|
151
|
+
s.restored.clear();
|
|
152
|
+
s.contributions.clear();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
(changed, owners)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Whole-element `restore`: replace everything with an unattributed set.
|
|
159
|
+
fn set_restored(&mut self, filters: Vec<Filter>) -> bool {
|
|
160
|
+
self.with_change(|s| {
|
|
161
|
+
s.contributions.clear();
|
|
162
|
+
s.restored = filters;
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// A single, fully-independent viewer-like unit within a [`Workspace`]: its own
|
|
168
|
+
/// [`Session`] (table binding + view config) and [`Renderer`] (active plugin).
|
|
169
|
+
#[derive(Clone)]
|
|
170
|
+
pub struct Panel {
|
|
171
|
+
pub id: PanelId,
|
|
172
|
+
pub session: Session,
|
|
173
|
+
pub renderer: Renderer,
|
|
174
|
+
|
|
175
|
+
/// Subscriptions owned for this panel's lifetime: its redraw subscription
|
|
176
|
+
/// (`table_updated` → redraw) plus its custom-event fanout
|
|
177
|
+
/// (`wire_panel_events`). Held here — not on the element — so they drop
|
|
178
|
+
/// exactly when the panel is removed from the [`Workspace`], with no
|
|
179
|
+
/// separate add/remove bookkeeping.
|
|
180
|
+
_subs: Rc<Vec<Subscription>>,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
impl Panel {
|
|
184
|
+
pub fn new(id: PanelId, session: Session, renderer: Renderer, subs: Vec<Subscription>) -> Self {
|
|
185
|
+
Self {
|
|
186
|
+
id,
|
|
187
|
+
session,
|
|
188
|
+
renderer,
|
|
189
|
+
_subs: Rc::new(subs),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// The multi-panel model backing a single `<perspective-viewer>`. See the
|
|
195
|
+
/// module docs for the Phase 1 (single-panel) invariants.
|
|
196
|
+
#[derive(Clone)]
|
|
197
|
+
pub struct Workspace(Rc<RefCell<WorkspaceData>>);
|
|
198
|
+
|
|
199
|
+
impl PartialEq for Workspace {
|
|
200
|
+
fn eq(&self, other: &Self) -> bool {
|
|
201
|
+
Rc::ptr_eq(&self.0, &other.0)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
struct WorkspaceData {
|
|
206
|
+
/// Panels in insertion order.
|
|
207
|
+
panels: Vec<Panel>,
|
|
208
|
+
|
|
209
|
+
/// The currently active/selected panel (a live panel in `panels`).
|
|
210
|
+
active: Option<PanelId>,
|
|
211
|
+
|
|
212
|
+
/// The first [`Client`] loaded via the element's `load()`.
|
|
213
|
+
default_client: Option<Client>,
|
|
214
|
+
|
|
215
|
+
/// Every [`Client`] ever loaded into this element (registration order.
|
|
216
|
+
clients: Vec<Client>,
|
|
217
|
+
|
|
218
|
+
/// Fires once per genuinely-new [`Client`] registration (post-dedup) —
|
|
219
|
+
/// the element subscribes each new client's hosted-tables updates for
|
|
220
|
+
/// reactive table binding (`tasks::table_lifecycle`).
|
|
221
|
+
client_registered: Rc<PubSub<Client>>,
|
|
222
|
+
|
|
223
|
+
/// Monotonic counter backing [`Workspace::generate_id`].
|
|
224
|
+
next_id: usize,
|
|
225
|
+
|
|
226
|
+
/// Panels designated as master/detail filter sources.
|
|
227
|
+
masters: HashSet<PanelId>,
|
|
228
|
+
|
|
229
|
+
/// The element-level global filters.
|
|
230
|
+
filters: GlobalFilterSet,
|
|
231
|
+
filters_changed: Rc<PubSub<()>>,
|
|
232
|
+
|
|
233
|
+
/// A layout tree staged by whole-element `restore`.
|
|
234
|
+
pending_layout: Option<crate::js::Layout>,
|
|
235
|
+
|
|
236
|
+
/// Freshly-created panels withheld from the `<regular-layout>`.
|
|
237
|
+
staged: HashSet<PanelId>,
|
|
238
|
+
staged_changed: Rc<PubSub<()>>,
|
|
239
|
+
|
|
240
|
+
/// The RESERVED first panel of a pending `load()`.
|
|
241
|
+
reserved: Option<Panel>,
|
|
242
|
+
|
|
243
|
+
/// In-flight effects (public mutators + scheduled internal flows),
|
|
244
|
+
/// drained by `flush()` — see [`EffectLedger`].
|
|
245
|
+
effects: EffectLedger,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
impl Default for Workspace {
|
|
249
|
+
fn default() -> Self {
|
|
250
|
+
Self::new()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
impl Workspace {
|
|
255
|
+
/// Create an EMPTY `Workspace`.
|
|
256
|
+
pub fn new() -> Self {
|
|
257
|
+
Self(Rc::new(RefCell::new(WorkspaceData {
|
|
258
|
+
panels: Vec::new(),
|
|
259
|
+
active: None,
|
|
260
|
+
default_client: None,
|
|
261
|
+
clients: Vec::new(),
|
|
262
|
+
client_registered: Rc::new(PubSub::default()),
|
|
263
|
+
next_id: 0,
|
|
264
|
+
masters: HashSet::new(),
|
|
265
|
+
filters: GlobalFilterSet::default(),
|
|
266
|
+
filters_changed: Rc::new(PubSub::default()),
|
|
267
|
+
pending_layout: None,
|
|
268
|
+
staged: HashSet::new(),
|
|
269
|
+
staged_changed: Rc::new(PubSub::default()),
|
|
270
|
+
reserved: None,
|
|
271
|
+
effects: EffectLedger::default(),
|
|
272
|
+
})))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// The element's in-flight effect ledger (see [`EffectLedger`]).
|
|
276
|
+
pub fn effects(&self) -> EffectLedger {
|
|
277
|
+
self.0.borrow().effects.clone()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// Stage a layout tree for `MainPanel` to apply at its next `rendered`
|
|
281
|
+
/// pass (see `WorkspaceData::pending_layout`).
|
|
282
|
+
pub fn set_pending_layout(&self, layout: crate::js::Layout) {
|
|
283
|
+
self.0.borrow_mut().pending_layout = Some(layout);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// Take (consume) the staged layout tree, if any.
|
|
287
|
+
pub fn take_pending_layout(&self) -> Option<crate::js::Layout> {
|
|
288
|
+
self.0.borrow_mut().pending_layout.take()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// A handle to the `staged_changed` PubSub (see
|
|
292
|
+
/// [`WorkspaceData::staged_changed`]).
|
|
293
|
+
pub fn staged_changed(&self) -> Rc<PubSub<()>> {
|
|
294
|
+
self.0.borrow().staged_changed.clone()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Mark a freshly-created panel STAGED (see [`WorkspaceData::staged`]).
|
|
298
|
+
/// Emits `staged_changed` — outside the borrow.
|
|
299
|
+
pub fn stage_panel(&self, id: &PanelId) {
|
|
300
|
+
let pubsub = {
|
|
301
|
+
let mut data = self.0.borrow_mut();
|
|
302
|
+
data.staged.insert(id.clone());
|
|
303
|
+
data.staged_changed.clone()
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
pubsub.emit(());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// Promote a staged panel toward layout insertion, returning whether it
|
|
310
|
+
/// was still staged — restore completion and the staging deadline RACE
|
|
311
|
+
/// to promote, and only the winner proceeds. Emits `staged_changed` —
|
|
312
|
+
/// outside the borrow — iff the set changed.
|
|
313
|
+
pub fn clear_staged(&self, id: &PanelId) -> bool {
|
|
314
|
+
let (removed, pubsub) = {
|
|
315
|
+
let mut data = self.0.borrow_mut();
|
|
316
|
+
(data.staged.remove(id), data.staged_changed.clone())
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
if removed {
|
|
320
|
+
pubsub.emit(());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
removed
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Whether `id` is a staged (created, not yet layout-inserted) panel.
|
|
327
|
+
pub fn is_staged(&self, id: &PanelId) -> bool {
|
|
328
|
+
self.0.borrow().staged.contains(id)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// The EFFECTIVE element-level global filters (master/detail
|
|
332
|
+
/// cross-filter): the restored bucket then each master's contribution,
|
|
333
|
+
/// deduped, as a snapshot clone.
|
|
334
|
+
pub fn global_filters(&self) -> Vec<Filter> {
|
|
335
|
+
self.0.borrow().filters.flatten()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// A handle to the `filters_changed` PubSub (fires after any change to the
|
|
339
|
+
/// global filter set).
|
|
340
|
+
pub fn filters_changed(&self) -> Rc<PubSub<()>> {
|
|
341
|
+
self.0.borrow().filters_changed.clone()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Run `f` against the filter set inside the borrow, then emit
|
|
345
|
+
/// `filters_changed` — outside it — iff the effective set changed.
|
|
346
|
+
/// Returns `f`'s auxiliary payload.
|
|
347
|
+
fn mutate_filters<T>(&self, f: impl FnOnce(&mut GlobalFilterSet) -> (bool, T)) -> T {
|
|
348
|
+
let (changed, payload, pubsub) = {
|
|
349
|
+
let mut data = self.0.borrow_mut();
|
|
350
|
+
let (changed, payload) = f(&mut data.filters);
|
|
351
|
+
(changed, payload, data.filters_changed.clone())
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if changed {
|
|
355
|
+
pubsub.emit(());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
payload
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Replace the global filter set with an UNATTRIBUTED (restored) bucket,
|
|
362
|
+
/// dropping every master contribution — the whole-element `restore`
|
|
363
|
+
/// entry point. Callers push the new set into panel sessions via
|
|
364
|
+
/// `tasks::apply_global_filters`.
|
|
365
|
+
pub fn set_global_filters(&self, filters: Vec<Filter>) {
|
|
366
|
+
self.mutate_filters(|s| (s.set_restored(filters), ()));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Replace master `id`'s contribution with a new selection (empty =
|
|
370
|
+
/// deselect). See [`GlobalFilterSet::set_contribution`].
|
|
371
|
+
pub fn set_contribution(&self, id: &PanelId, filters: Vec<Filter>) {
|
|
372
|
+
self.mutate_filters(|s| (s.set_contribution(id, filters), ()));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// Drop master `id`'s contribution (deselect / demote / close).
|
|
376
|
+
pub fn clear_contribution(&self, id: &PanelId) {
|
|
377
|
+
self.mutate_filters(|s| (s.set_contribution(id, Vec::new()), ()));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// Remove the effective-set clause at `index` (the `GlobalFilterBar`
|
|
381
|
+
/// chip ×), returning the OWNING master panels of the removed clause so
|
|
382
|
+
/// the caller can clear their selection state
|
|
383
|
+
/// (`tasks::clear_master_selections`).
|
|
384
|
+
pub fn remove_global_filter(&self, index: usize) -> Vec<PanelId> {
|
|
385
|
+
self.mutate_filters(|s| s.remove_clause(index))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Drop the entire global filter set (the `GlobalFilterBar` "Clear" /
|
|
389
|
+
/// element `reset()`), returning the contribution owners for
|
|
390
|
+
/// selection-state cleanup. Master ROLES are untouched — like the
|
|
391
|
+
/// layout, they are workspace structure, not filter state.
|
|
392
|
+
pub fn clear_global_filters(&self) -> Vec<PanelId> {
|
|
393
|
+
self.mutate_filters(GlobalFilterSet::clear)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// The master (filter-source) panel ids, SORTED — `save()` serializes
|
|
397
|
+
/// this, and a `HashSet`'s per-instance iteration order would make
|
|
398
|
+
/// consecutive `save()` outputs byte-unstable (cf. the `panels`
|
|
399
|
+
/// `BTreeMap`).
|
|
400
|
+
pub fn masters(&self) -> Vec<PanelId> {
|
|
401
|
+
let mut masters: Vec<_> = self.0.borrow().masters.iter().cloned().collect();
|
|
402
|
+
masters.sort();
|
|
403
|
+
masters
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Replace the master role set — the whole-element `restore` entry point
|
|
407
|
+
/// (ids already remapped to the fresh panel ids).
|
|
408
|
+
pub fn set_masters(&self, ids: Vec<PanelId>) {
|
|
409
|
+
self.0.borrow_mut().masters = ids.into_iter().collect();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Whether `id` is a master (filter-source) panel.
|
|
413
|
+
pub fn is_master(&self, id: &PanelId) -> bool {
|
|
414
|
+
self.0.borrow().masters.contains(id)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// Toggle `id`'s master/detail role, returning the new state (`true` =
|
|
418
|
+
/// master).
|
|
419
|
+
pub fn toggle_master(&self, id: &PanelId) -> bool {
|
|
420
|
+
let mut data = self.0.borrow_mut();
|
|
421
|
+
if data.masters.remove(id) {
|
|
422
|
+
false
|
|
423
|
+
} else {
|
|
424
|
+
data.masters.insert(id.clone());
|
|
425
|
+
true
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Generate a fresh [`PanelId`], unique within this workspace.
|
|
430
|
+
pub fn generate_id(&self) -> PanelId {
|
|
431
|
+
let mut data = self.0.borrow_mut();
|
|
432
|
+
let n = data.next_id;
|
|
433
|
+
data.next_id += 1;
|
|
434
|
+
PanelId(format!("PERSPECTIVE_GENERATED_ID_{n}"))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// The id of the active panel, or `None` with zero panels.
|
|
438
|
+
pub fn active_id(&self) -> Option<PanelId> {
|
|
439
|
+
self.0.borrow().active.clone()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// The active [`Panel`] (clone; shares engine state), or `None` with zero
|
|
443
|
+
/// panels.
|
|
444
|
+
pub fn active_panel(&self) -> Option<Panel> {
|
|
445
|
+
let data = self.0.borrow();
|
|
446
|
+
let active = data.active.as_ref()?;
|
|
447
|
+
data.panels.iter().find(|p| &p.id == active).cloned()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// The active panel's [`Session`], or `None` with zero panels.
|
|
451
|
+
pub fn active_session(&self) -> Option<Session> {
|
|
452
|
+
self.active_panel().map(|p| p.session)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/// The active panel's [`Renderer`], or `None` with zero panels.
|
|
456
|
+
pub fn active_renderer(&self) -> Option<Renderer> {
|
|
457
|
+
self.active_panel().map(|p| p.renderer)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/// Look up a [`Panel`] by id.
|
|
461
|
+
pub fn panel(&self, id: &PanelId) -> Option<Panel> {
|
|
462
|
+
self.0.borrow().panels.iter().find(|p| &p.id == id).cloned()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/// Resolve a panel by id, or the active panel when `id` is `None` — the
|
|
466
|
+
/// shape the public `*Panel(name?)` delegating methods will use.
|
|
467
|
+
pub fn panel_or_active(&self, id: Option<&PanelId>) -> Option<Panel> {
|
|
468
|
+
match id {
|
|
469
|
+
Some(id) => self.panel(id),
|
|
470
|
+
None => self.active_panel(),
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// Snapshot of all [`Panel`]s, in insertion order — the canonical
|
|
475
|
+
/// fan-out source (fan-outs collect per-panel results; they never
|
|
476
|
+
/// sequential-abort on one panel's error).
|
|
477
|
+
pub fn panels(&self) -> Vec<Panel> {
|
|
478
|
+
self.0.borrow().panels.to_vec()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// The number of PLACED panels (panels minus staged) — the single
|
|
482
|
+
/// source for panel-count chrome (`single`/`multi`, closable,
|
|
483
|
+
/// draggable, `only-child`).
|
|
484
|
+
pub fn placed_count(&self) -> usize {
|
|
485
|
+
let data = self.0.borrow();
|
|
486
|
+
data.panels
|
|
487
|
+
.iter()
|
|
488
|
+
.filter(|p| !data.staged.contains(&p.id))
|
|
489
|
+
.count()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/// All panel ids, in insertion order.
|
|
493
|
+
pub fn panel_ids(&self) -> Vec<PanelId> {
|
|
494
|
+
self.0
|
|
495
|
+
.borrow()
|
|
496
|
+
.panels
|
|
497
|
+
.iter()
|
|
498
|
+
.map(|p| p.id.clone())
|
|
499
|
+
.collect()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/// The number of panels (may be `0` — an empty stage).
|
|
503
|
+
pub fn len(&self) -> usize {
|
|
504
|
+
self.0.borrow().panels.len()
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/// Whether the element has zero panels.
|
|
508
|
+
pub fn is_empty(&self) -> bool {
|
|
509
|
+
self.0.borrow().panels.is_empty()
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/// Append a [`Panel`]. When the element had zero panels, the inserted panel
|
|
513
|
+
/// becomes the active one (there is no other candidate).
|
|
514
|
+
pub fn insert_panel(&self, panel: Panel) {
|
|
515
|
+
let mut data = self.0.borrow_mut();
|
|
516
|
+
if data.active.is_none() {
|
|
517
|
+
data.active = Some(panel.id.clone());
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
panel
|
|
521
|
+
.renderer
|
|
522
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
523
|
+
data.panels.push(panel);
|
|
524
|
+
Self::sync_solo_flags(&data);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// Hold `panel` in the reservation slot (see [`WorkspaceData::reserved`]):
|
|
528
|
+
/// NOT placed, invisible to every placed-panel consumer, awaiting a
|
|
529
|
+
/// pending `load()`'s payload classification or an interim claimant.
|
|
530
|
+
pub fn reserve_panel(&self, panel: Panel) {
|
|
531
|
+
self.0.borrow_mut().reserved = Some(panel);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/// The reservation slot's current occupant (shared handles), without a
|
|
535
|
+
/// transfer — a second `load()` on a still-empty element adopts the same
|
|
536
|
+
/// reservation rather than creating a competing one.
|
|
537
|
+
pub fn reserved_panel(&self) -> Option<Panel> {
|
|
538
|
+
self.0.borrow().reserved.clone()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/// PLACE the reserved panel: transfer it out of the reservation slot into
|
|
542
|
+
/// the placed set ([`Self::insert_panel`] — auto-activating on an empty
|
|
543
|
+
/// element). `None` when the slot is empty (no reservation, or the other
|
|
544
|
+
/// actor transferred first).
|
|
545
|
+
pub fn claim_reserved(&self) -> Option<Panel> {
|
|
546
|
+
let panel = self.0.borrow_mut().reserved.take()?;
|
|
547
|
+
self.insert_panel(panel.clone());
|
|
548
|
+
Some(panel)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/// DISCARD the reserved panel: transfer it out of the reservation slot
|
|
552
|
+
/// WITHOUT placing it, for disposal (an inert `Client` payload with no
|
|
553
|
+
/// claimant, or teardown draining). `None` when the slot is empty.
|
|
554
|
+
pub fn take_reserved(&self) -> Option<Panel> {
|
|
555
|
+
self.0.borrow_mut().reserved.take()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/// Sync every panel renderer's solo (lone-panel) flag with the current
|
|
559
|
+
/// panel count — called from each count-changing mutation site (data
|
|
560
|
+
/// only; the `single`/`multi` CSS classes land inside each panel's next
|
|
561
|
+
/// locked plugin dispatch — see `Renderer::stamp_active`).
|
|
562
|
+
fn sync_solo_flags(data: &WorkspaceData) {
|
|
563
|
+
let is_solo = data.panels.len() == 1;
|
|
564
|
+
for panel in data.panels.iter() {
|
|
565
|
+
panel.renderer.set_solo_flag(is_solo);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Remove a [`Panel`] by id, returning it if present.
|
|
570
|
+
/// Remove a [`Panel`] by id, returning it if present. Model cleanup is
|
|
571
|
+
/// structural: EVERY removal path (close, whole-element restore's batch
|
|
572
|
+
/// replacement) drops the panel's master role and its global-filter
|
|
573
|
+
/// contribution here, so neither can outlive the panel.
|
|
574
|
+
pub fn remove_panel(&self, id: &PanelId) -> Option<Panel> {
|
|
575
|
+
let (removed, changed, pubsub, staged_removed, staged_pubsub) = {
|
|
576
|
+
let mut data = self.0.borrow_mut();
|
|
577
|
+
data.masters.remove(id);
|
|
578
|
+
let staged_removed = data.staged.remove(id);
|
|
579
|
+
let changed = data.filters.set_contribution(id, Vec::new());
|
|
580
|
+
let removed = data
|
|
581
|
+
.panels
|
|
582
|
+
.iter()
|
|
583
|
+
.position(|p| &p.id == id)
|
|
584
|
+
.map(|idx| data.panels.remove(idx));
|
|
585
|
+
|
|
586
|
+
if data.active.as_ref() == Some(id) {
|
|
587
|
+
data.active = None;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
Self::sync_solo_flags(&data);
|
|
591
|
+
(
|
|
592
|
+
removed,
|
|
593
|
+
changed,
|
|
594
|
+
data.filters_changed.clone(),
|
|
595
|
+
staged_removed,
|
|
596
|
+
data.staged_changed.clone(),
|
|
597
|
+
)
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
if changed {
|
|
601
|
+
pubsub.emit(());
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if staged_removed {
|
|
605
|
+
staged_pubsub.emit(());
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
removed
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/// Set the active panel. Returns `false` (no-op) if `id` is not a known
|
|
612
|
+
/// panel.
|
|
613
|
+
pub fn set_active(&self, id: PanelId) -> bool {
|
|
614
|
+
let mut data = self.0.borrow_mut();
|
|
615
|
+
if data.panels.iter().any(|p| p.id == id) {
|
|
616
|
+
data.active = Some(id);
|
|
617
|
+
for panel in data.panels.iter() {
|
|
618
|
+
panel
|
|
619
|
+
.renderer
|
|
620
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
true
|
|
624
|
+
} else {
|
|
625
|
+
false
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/// The default [`Client`], if one has been loaded.
|
|
630
|
+
pub fn default_client(&self) -> Option<Client> {
|
|
631
|
+
self.0.borrow().default_client.clone()
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/// The active panel's bound [`Client`], if any — the default target of a
|
|
635
|
+
/// no-argument `eject()`.
|
|
636
|
+
pub fn active_client(&self) -> Option<Client> {
|
|
637
|
+
self.active_panel().and_then(|p| p.session.get_client())
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/// The ids of every panel whose session is bound to the [`Client`] named
|
|
641
|
+
/// `name` (client names are globally unique), in insertion order.
|
|
642
|
+
pub fn panels_for_client(&self, name: &str) -> Vec<PanelId> {
|
|
643
|
+
self.0
|
|
644
|
+
.borrow()
|
|
645
|
+
.panels
|
|
646
|
+
.iter()
|
|
647
|
+
.filter(|p| p.session.get_client().is_some_and(|c| c.get_name() == name))
|
|
648
|
+
.map(|p| p.id.clone())
|
|
649
|
+
.collect()
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/// Drop the [`Client`] named `name` from the loaded-clients registry, and
|
|
653
|
+
/// clear the default-client designation if it referred to this client.
|
|
654
|
+
/// Callers must have already removed every panel bound to it (see
|
|
655
|
+
/// [`Workspace::panels_for_client`]) — `clients()` unions in live panel
|
|
656
|
+
/// sessions, so a lingering panel would resurrect it.
|
|
657
|
+
pub fn remove_client(&self, name: &str) {
|
|
658
|
+
let mut data = self.0.borrow_mut();
|
|
659
|
+
data.clients.retain(|c| c.get_name() != name);
|
|
660
|
+
if data
|
|
661
|
+
.default_client
|
|
662
|
+
.as_ref()
|
|
663
|
+
.is_some_and(|c| c.get_name() == name)
|
|
664
|
+
{
|
|
665
|
+
data.default_client = None;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/// Record the default [`Client`] if not already set (first-wins, matching
|
|
670
|
+
/// the "first `Client` passed to `load()` is the default" rule). Always
|
|
671
|
+
/// registers the client (see [`Workspace::register_client`]) — first-wins
|
|
672
|
+
/// applies only to the *default* designation.
|
|
673
|
+
pub fn set_default_client(&self, client: Client) {
|
|
674
|
+
self.register_client(client.clone());
|
|
675
|
+
let mut data = self.0.borrow_mut();
|
|
676
|
+
if data.default_client.is_none() {
|
|
677
|
+
data.default_client = Some(client);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/// Add a [`Client`] to the loaded-clients registry, if a client with the
|
|
682
|
+
/// same (globally unique) name isn't already present. Emits
|
|
683
|
+
/// `client_registered` — outside the borrow — for a genuinely-new client.
|
|
684
|
+
pub fn register_client(&self, client: Client) {
|
|
685
|
+
let pubsub = {
|
|
686
|
+
let mut data = self.0.borrow_mut();
|
|
687
|
+
if data
|
|
688
|
+
.clients
|
|
689
|
+
.iter()
|
|
690
|
+
.any(|c| c.get_name() == client.get_name())
|
|
691
|
+
{
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
data.clients.push(client.clone());
|
|
696
|
+
data.client_registered.clone()
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
pubsub.emit(client);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/// A handle to the `client_registered` PubSub (see
|
|
703
|
+
/// [`WorkspaceData::client_registered`]).
|
|
704
|
+
pub fn client_registered(&self) -> Rc<PubSub<Client>> {
|
|
705
|
+
self.0.borrow().client_registered.clone()
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/// All loaded [`Client`]s: the registry, unioned with every panel
|
|
709
|
+
/// session's bound client (belt-and-braces for any binding path that
|
|
710
|
+
/// bypasses registration), deduped by name in registration order.
|
|
711
|
+
pub fn clients(&self) -> Vec<Client> {
|
|
712
|
+
let data = self.0.borrow();
|
|
713
|
+
let mut clients = data.clients.clone();
|
|
714
|
+
for panel in &data.panels {
|
|
715
|
+
if let Some(client) = panel.session.get_client()
|
|
716
|
+
&& !clients.iter().any(|c| c.get_name() == client.get_name())
|
|
717
|
+
{
|
|
718
|
+
clients.push(client);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
clients
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/// Resolve which loaded [`Client`] a panel should bind to serve the `Table`
|
|
726
|
+
/// named `table_name`, given its `current` binding (if any).
|
|
727
|
+
pub async fn resolve_client_for_table(
|
|
728
|
+
&self,
|
|
729
|
+
table_name: &str,
|
|
730
|
+
current: Option<&Client>,
|
|
731
|
+
) -> Option<Client> {
|
|
732
|
+
let clients = self.clients();
|
|
733
|
+
|
|
734
|
+
// A single bound client is already correct — no probe, no rebind.
|
|
735
|
+
if current.is_some() && clients.len() <= 1 {
|
|
736
|
+
return None;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let mut host = None;
|
|
740
|
+
for client in &clients {
|
|
741
|
+
if let Ok(names) = client.get_hosted_table_names().await
|
|
742
|
+
&& names.iter().any(|n| n.as_str() == table_name)
|
|
743
|
+
{
|
|
744
|
+
host = Some(client.clone());
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
match host {
|
|
750
|
+
Some(host) if current.map(|c| c.get_name()) != Some(host.get_name()) => Some(host),
|
|
751
|
+
Some(_) => None,
|
|
752
|
+
// No host: seed an unbound session with the default client; leave a
|
|
753
|
+
// bound session as-is.
|
|
754
|
+
None => current.is_none().then(|| self.default_client()).flatten(),
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#[cfg(test)]
|
|
760
|
+
mod tests {
|
|
761
|
+
use perspective_client::config::FilterTerm;
|
|
762
|
+
|
|
763
|
+
use super::*;
|
|
764
|
+
|
|
765
|
+
fn f(col: &str, term: &str) -> Filter {
|
|
766
|
+
Filter::new(col, "==", FilterTerm::Scalar(term.into()))
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
fn p(id: &str) -> PanelId {
|
|
770
|
+
PanelId::from(id)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
#[test]
|
|
774
|
+
fn flatten_orders_and_dedups_across_buckets() {
|
|
775
|
+
let mut s = GlobalFilterSet::default();
|
|
776
|
+
assert!(s.set_restored(vec![f("a", "1"), f("b", "2")]));
|
|
777
|
+
// A non-empty contribution drops the restored bucket entirely.
|
|
778
|
+
assert!(s.set_contribution(&p("x"), vec![f("b", "2"), f("c", "3")]));
|
|
779
|
+
assert_eq!(s.flatten(), vec![f("b", "2"), f("c", "3")]);
|
|
780
|
+
assert!(s.set_contribution(&p("y"), vec![f("c", "3"), f("d", "4")]));
|
|
781
|
+
assert_eq!(s.flatten(), vec![f("b", "2"), f("c", "3"), f("d", "4")]);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#[test]
|
|
785
|
+
fn set_contribution_replaces_not_accumulates() {
|
|
786
|
+
let mut s = GlobalFilterSet::default();
|
|
787
|
+
assert!(s.set_contribution(&p("x"), vec![f("a", "1")]));
|
|
788
|
+
assert!(s.set_contribution(&p("x"), vec![f("a", "2")]));
|
|
789
|
+
assert_eq!(s.flatten(), vec![f("a", "2")]);
|
|
790
|
+
// Re-selecting the same value reports no visible change.
|
|
791
|
+
assert!(!s.set_contribution(&p("x"), vec![f("a", "2")]));
|
|
792
|
+
// Empty = deselect: the entry is removed.
|
|
793
|
+
assert!(s.set_contribution(&p("x"), Vec::new()));
|
|
794
|
+
assert_eq!(s.flatten(), Vec::<Filter>::new());
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
#[test]
|
|
798
|
+
fn contributions_are_per_panel() {
|
|
799
|
+
let mut s = GlobalFilterSet::default();
|
|
800
|
+
s.set_contribution(&p("x"), vec![f("a", "1")]);
|
|
801
|
+
s.set_contribution(&p("y"), vec![f("b", "2")]);
|
|
802
|
+
// Clearing one master's contribution leaves the other's intact.
|
|
803
|
+
assert!(s.set_contribution(&p("x"), Vec::new()));
|
|
804
|
+
assert_eq!(s.flatten(), vec![f("b", "2")]);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#[test]
|
|
808
|
+
fn remove_clause_removes_from_every_bucket_and_reports_owners() {
|
|
809
|
+
let mut s = GlobalFilterSet::default();
|
|
810
|
+
s.set_contribution(&p("x"), vec![f("a", "1"), f("b", "2")]);
|
|
811
|
+
s.set_contribution(&p("y"), vec![f("a", "1")]);
|
|
812
|
+
// flatten = [a==1, b==2]; removing index 0 must drop BOTH copies of
|
|
813
|
+
// a==1 (owners x AND y), and the now-empty "y" entry with it.
|
|
814
|
+
let (changed, owners) = s.remove_clause(0);
|
|
815
|
+
assert!(changed);
|
|
816
|
+
assert_eq!(owners, vec![p("x"), p("y")]);
|
|
817
|
+
assert_eq!(s.flatten(), vec![f("b", "2")]);
|
|
818
|
+
// Out-of-range is a no-op.
|
|
819
|
+
assert_eq!(s.remove_clause(5), (false, Vec::new()));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
#[test]
|
|
823
|
+
fn restored_bucket_survives_deselect_but_not_selection() {
|
|
824
|
+
let mut s = GlobalFilterSet::default();
|
|
825
|
+
s.set_restored(vec![f("a", "1")]);
|
|
826
|
+
// A deselect (empty contribution) does NOT drop the restored bucket.
|
|
827
|
+
assert!(!s.set_contribution(&p("x"), Vec::new()));
|
|
828
|
+
assert_eq!(s.flatten(), vec![f("a", "1")]);
|
|
829
|
+
// A real selection replaces it.
|
|
830
|
+
assert!(s.set_contribution(&p("x"), vec![f("b", "2")]));
|
|
831
|
+
assert_eq!(s.flatten(), vec![f("b", "2")]);
|
|
832
|
+
assert!(s.restored.is_empty());
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
#[test]
|
|
836
|
+
fn clear_drops_everything_and_reports_owners() {
|
|
837
|
+
let mut s = GlobalFilterSet::default();
|
|
838
|
+
s.set_restored(vec![f("a", "1")]);
|
|
839
|
+
s.set_contribution(&p("x"), vec![f("b", "2")]);
|
|
840
|
+
let (changed, owners) = s.clear();
|
|
841
|
+
assert!(changed);
|
|
842
|
+
assert_eq!(owners, vec![p("x")]);
|
|
843
|
+
assert_eq!(s.flatten(), Vec::<Filter>::new());
|
|
844
|
+
assert_eq!(s.clear(), (false, Vec::new()));
|
|
845
|
+
}
|
|
846
|
+
}
|