@perspective-dev/viewer 5.1.0 → 5.3.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 +9 -9
- 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/docs/perspective-docs.json +1 -0
- package/dist/esm/column-format.d.ts +19 -28
- package/dist/esm/perspective-viewer.d.ts +3 -0
- package/dist/esm/perspective-viewer.inline.js +9 -9
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +10 -0
- package/dist/esm/providers.d.ts +79 -0
- package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
- package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
- package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
- package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
- package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
- package/dist/esm/ts-rs/Notation.d.ts +17 -0
- package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +42 -2
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
- package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
- package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
- package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
- package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
- package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
- package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/Unit.d.ts +1 -0
- package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
- package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
- package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +42 -2
- package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
- package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +20 -9
- package/dist/wasm/perspective-viewer.d.ts +157 -41
- package/dist/wasm/perspective-viewer.js +452 -151
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
- package/package.json +2 -1
- package/src/css/_viewer_bundle.css +1 -0
- package/src/css/column-selector.css +8 -0
- package/src/css/column-settings-panel.css +9 -5
- package/src/css/column-style.css +209 -31
- package/src/css/config-selector.css +38 -3
- package/src/css/dom/select.css +2 -1
- package/src/css/form/code-editor.css +6 -7
- package/src/css/form/mirrored-textarea.css +81 -0
- package/src/css/viewer.css +328 -0
- package/src/rust/agent/client/mod.rs +19 -0
- package/src/rust/agent/client/protocol.rs +320 -0
- package/src/rust/agent/client/transport.rs +287 -0
- package/src/rust/agent/client/turn.rs +174 -0
- package/src/rust/agent/config.rs +159 -0
- package/src/rust/agent/docs.rs +293 -0
- package/src/rust/agent/markdown/block.rs +509 -0
- package/src/rust/agent/markdown/entity.rs +52 -0
- package/src/rust/agent/markdown/events.rs +71 -0
- package/src/rust/agent/markdown/inline.rs +497 -0
- package/src/rust/agent/markdown/mod.rs +466 -0
- package/src/rust/agent/mod.rs +23 -0
- package/src/rust/agent/preamble.md +16 -0
- package/src/rust/agent/runtime.rs +201 -0
- package/src/rust/agent/slot.rs +236 -0
- package/src/rust/agent/tools.rs +1141 -0
- package/src/rust/components/chat_panel.rs +289 -0
- package/src/rust/components/column_selector/active_column.rs +2 -2
- package/src/rust/components/column_selector/add_expression_button.rs +3 -3
- package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
- package/src/rust/components/column_selector/config_selector.rs +84 -7
- package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
- package/src/rust/components/column_selector/inactive_column.rs +2 -2
- package/src/rust/components/column_selector.rs +55 -4
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
- package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
- package/src/rust/components/column_settings_sidebar.rs +88 -69
- package/src/rust/components/containers/dragdrop_list.rs +37 -0
- package/src/rust/components/editable_header.rs +85 -144
- package/src/rust/components/expression_editor.rs +9 -16
- package/src/rust/components/form/code_editor.rs +29 -28
- package/src/rust/components/form/debug.rs +9 -5
- package/src/rust/components/form/mirrored_textarea.rs +165 -0
- package/src/rust/components/form/mod.rs +4 -1
- package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
- package/src/rust/components/form/named_value_picker.rs +86 -0
- package/src/rust/components/form/palette_selector.rs +440 -0
- package/src/rust/components/main_panel/msg.rs +2 -2
- package/src/rust/components/main_panel/reconcile.rs +4 -4
- package/src/rust/components/mod.rs +2 -0
- package/src/rust/components/plugin_tab.rs +4 -28
- package/src/rust/components/settings_panel.rs +105 -22
- package/src/rust/components/viewer/msg.rs +46 -7
- package/src/rust/components/viewer/panels.rs +17 -20
- package/src/rust/components/viewer/render.rs +10 -3
- package/src/rust/components/viewer/settings.rs +163 -23
- package/src/rust/components/viewer/snapshots.rs +46 -21
- package/src/rust/components/viewer/wiring.rs +14 -25
- package/src/rust/components/viewer.rs +12 -8
- package/src/rust/components/window_editor.rs +75 -103
- package/src/rust/config/column_config_schema.rs +100 -41
- package/src/rust/config/css_scale.rs +882 -0
- package/src/rust/config/datetime_column_style/custom.rs +17 -0
- package/src/rust/config/datetime_column_style/simple.rs +10 -0
- package/src/rust/config/mod.rs +2 -0
- package/src/rust/config/number_string_format.rs +33 -0
- package/src/rust/config/options.rs +28 -0
- package/src/rust/config/plugin_static_config.rs +120 -5
- package/src/rust/config/viewer_config.rs +216 -3
- package/src/rust/config/workspace_config.rs +138 -10
- package/src/rust/custom_elements/viewer.rs +369 -119
- package/src/rust/custom_events.rs +43 -1
- package/src/rust/js/regular_layout.rs +3 -3
- package/src/rust/lib.rs +15 -0
- package/src/rust/presentation/column_locator.rs +19 -5
- package/src/rust/presentation.rs +200 -36
- package/src/rust/queries/column_locator.rs +74 -44
- package/src/rust/queries/get_viewer_config.rs +2 -1
- package/src/rust/queries/mod.rs +2 -0
- package/src/rust/queries/palette_set.rs +82 -0
- package/src/rust/queries/plugin_column_styles.rs +6 -2
- package/src/rust/renderer/dispatch.rs +16 -4
- package/src/rust/renderer/plugin_config.rs +309 -47
- package/src/rust/renderer.rs +15 -42
- package/src/rust/session/column_defaults_update.rs +32 -7
- package/src/rust/session/metadata.rs +23 -21
- package/src/rust/session/props.rs +1 -0
- package/src/rust/session/replace_expression_update.rs +1 -0
- package/src/rust/session.rs +9 -6
- package/src/rust/tasks/create_panel.rs +46 -49
- package/src/rust/tasks/edit_expression.rs +19 -23
- package/src/rust/tasks/edit_window.rs +9 -11
- package/src/rust/tasks/eject.rs +4 -1
- package/src/rust/tasks/pipeline.rs +2 -2
- package/src/rust/tasks/presize_panels.rs +44 -5
- package/src/rust/tasks/reset_all.rs +4 -4
- package/src/rust/tasks/restore_and_render.rs +17 -44
- package/src/rust/tasks/restore_panel.rs +37 -22
- package/src/rust/tasks/set_edit_mode.rs +3 -1
- package/src/rust/tasks/sync_update_panels.rs +18 -24
- package/src/rust/tasks/update_theme.rs +23 -18
- package/src/rust/utils/css_vars.rs +26 -0
- package/src/rust/utils/mod.rs +2 -0
- package/src/rust/utils/spawn.rs +1 -1
- package/src/rust/workspace.rs +454 -199
- package/src/svg/lock.svg +7 -0
- package/src/themes/botanical.css +10 -0
- package/src/themes/defaults.css +12 -0
- package/src/themes/dracula.css +17 -0
- package/src/themes/gruvbox-dark.css +20 -0
- package/src/themes/gruvbox.css +10 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/de.css +16 -0
- package/src/themes/intl/es.css +16 -0
- package/src/themes/intl/fr.css +16 -0
- package/src/themes/intl/ja.css +16 -0
- package/src/themes/intl/pt.css +16 -0
- package/src/themes/intl/zh.css +16 -0
- package/src/themes/intl.css +16 -0
- package/src/themes/monokai.css +22 -0
- package/src/themes/phosphor.css +5 -0
- package/src/themes/pro-dark.css +10 -0
- package/src/themes/solarized-dark.css +7 -0
- package/src/themes/solarized.css +7 -0
- package/src/themes/vaporwave.css +20 -0
- package/src/ts/column-format.ts +67 -46
- package/src/ts/perspective-viewer.ts +3 -0
- package/src/ts/plugin.ts +14 -0
- package/src/ts/providers.ts +96 -0
- package/src/ts/ts-rs/CompactDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyCode.ts +3 -0
- package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
- package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
- package/src/ts/ts-rs/CurrencySign.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
- package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
- package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
- package/src/ts/ts-rs/FormatMode.ts +3 -0
- package/src/ts/ts-rs/FormatUnit.ts +3 -0
- package/src/ts/ts-rs/Notation.ts +11 -0
- package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +48 -2
- package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
- package/src/ts/ts-rs/RestoreOptions.ts +20 -0
- package/src/ts/ts-rs/RoundingMode.ts +3 -0
- package/src/ts/ts-rs/RoundingPriority.ts +3 -0
- package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
- package/src/ts/ts-rs/SignDisplay.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
- package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
- package/src/ts/ts-rs/StringColorMode.ts +3 -0
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
- package/src/ts/ts-rs/Unit.ts +3 -0
- package/src/ts/ts-rs/UnitDisplay.ts +3 -0
- package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
- package/src/ts/ts-rs/UseGrouping.ts +3 -0
- package/src/ts/ts-rs/ViewerConfig.ts +48 -2
- package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
- package/src/ts/ts-rs/WindowSpec.ts +3 -2
- package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +18 -9
- package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
- package/src/rust/components/form/color_range_selector.rs +0 -137
- package/src/ts/ts-rs/WindowAggregate.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
package/src/rust/workspace.rs
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
//! The multi-panel model backing a single `<perspective-viewer>`.
|
|
14
14
|
|
|
15
|
-
use std::cell::RefCell;
|
|
16
|
-
use std::collections::HashSet;
|
|
15
|
+
use std::cell::{Cell, RefCell};
|
|
17
16
|
use std::rc::Rc;
|
|
18
17
|
|
|
19
18
|
use perspective_client::Client;
|
|
@@ -21,7 +20,7 @@ use perspective_client::config::Filter;
|
|
|
21
20
|
|
|
22
21
|
use crate::renderer::Renderer;
|
|
23
22
|
use crate::session::Session;
|
|
24
|
-
use crate::utils::{EffectLedger, PubSub, Subscription};
|
|
23
|
+
use crate::utils::{EffectLedger, PubSub, Subscription, spawn_owned};
|
|
25
24
|
|
|
26
25
|
/// A unique identifier for a [`Panel`] within a [`Workspace`].
|
|
27
26
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
@@ -51,12 +50,7 @@ impl From<&str> for PanelId {
|
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
/// The element-level global filter state (master/detail cross-filter
|
|
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.
|
|
53
|
+
/// The element-level global filter state (master/detail cross-filter.
|
|
60
54
|
#[derive(Default)]
|
|
61
55
|
struct GlobalFilterSet {
|
|
62
56
|
restored: Vec<Filter>,
|
|
@@ -91,10 +85,7 @@ impl GlobalFilterSet {
|
|
|
91
85
|
before != self.flatten()
|
|
92
86
|
}
|
|
93
87
|
|
|
94
|
-
/// Replace `id`'s contribution
|
|
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).
|
|
88
|
+
/// Replace `id`'s contribution.
|
|
98
89
|
fn set_contribution(&mut self, id: &PanelId, filters: Vec<Filter>) -> bool {
|
|
99
90
|
self.with_change(|s| {
|
|
100
91
|
if filters.is_empty() {
|
|
@@ -109,11 +100,7 @@ impl GlobalFilterSet {
|
|
|
109
100
|
})
|
|
110
101
|
}
|
|
111
102
|
|
|
112
|
-
/// Remove the flattened-view clause at `index` from EVERY bucket
|
|
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.
|
|
103
|
+
/// Remove the flattened-view clause at `index` from EVERY bucket.
|
|
117
104
|
fn remove_clause(&mut self, index: usize) -> (bool, Vec<PanelId>) {
|
|
118
105
|
let Some(clause) = self.flatten().get(index).cloned() else {
|
|
119
106
|
return (false, Vec::new());
|
|
@@ -155,7 +142,7 @@ impl GlobalFilterSet {
|
|
|
155
142
|
(changed, owners)
|
|
156
143
|
}
|
|
157
144
|
|
|
158
|
-
///
|
|
145
|
+
/// `restoreWorkspace`: replace everything with an unattributed set.
|
|
159
146
|
fn set_restored(&mut self, filters: Vec<Filter>) -> bool {
|
|
160
147
|
self.with_change(|s| {
|
|
161
148
|
s.contributions.clear();
|
|
@@ -164,19 +151,12 @@ impl GlobalFilterSet {
|
|
|
164
151
|
}
|
|
165
152
|
}
|
|
166
153
|
|
|
167
|
-
/// A single, fully-independent viewer-like unit within a [`Workspace`]
|
|
168
|
-
/// [`Session`] (table binding + view config) and [`Renderer`] (active plugin).
|
|
154
|
+
/// A single, fully-independent viewer-like unit within a [`Workspace`].
|
|
169
155
|
#[derive(Clone)]
|
|
170
156
|
pub struct Panel {
|
|
171
157
|
pub id: PanelId,
|
|
172
158
|
pub session: Session,
|
|
173
159
|
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
160
|
_subs: Rc<Vec<Subscription>>,
|
|
181
161
|
}
|
|
182
162
|
|
|
@@ -191,6 +171,120 @@ impl Panel {
|
|
|
191
171
|
}
|
|
192
172
|
}
|
|
193
173
|
|
|
174
|
+
/// A placed panel's layout phase.
|
|
175
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
176
|
+
pub enum PanelPhase {
|
|
177
|
+
Staging,
|
|
178
|
+
Placed,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// A [`Panel`] as registered in the placed set.
|
|
182
|
+
struct PanelEntry {
|
|
183
|
+
panel: Panel,
|
|
184
|
+
phase: PanelPhase,
|
|
185
|
+
master: bool,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[derive(Default)]
|
|
189
|
+
enum Reservation {
|
|
190
|
+
#[default]
|
|
191
|
+
Idle,
|
|
192
|
+
Pending(Panel),
|
|
193
|
+
Claimed {
|
|
194
|
+
has_table: bool,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
199
|
+
enum FlushState {
|
|
200
|
+
#[default]
|
|
201
|
+
Idle,
|
|
202
|
+
Queued {
|
|
203
|
+
layout: bool,
|
|
204
|
+
active: bool,
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
impl FlushState {
|
|
209
|
+
/// Merge newly-dirty events, returning the next state and whether a flush
|
|
210
|
+
/// task must be spawned.
|
|
211
|
+
fn mark(self, layout: bool, active: bool) -> (Self, bool) {
|
|
212
|
+
match self {
|
|
213
|
+
_ if !layout && !active => (self, false),
|
|
214
|
+
Self::Idle => (Self::Queued { layout, active }, true),
|
|
215
|
+
Self::Queued {
|
|
216
|
+
layout: l,
|
|
217
|
+
active: a,
|
|
218
|
+
} => (
|
|
219
|
+
Self::Queued {
|
|
220
|
+
layout: l || layout,
|
|
221
|
+
active: a || active,
|
|
222
|
+
},
|
|
223
|
+
false,
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[derive(Default)]
|
|
230
|
+
struct LayoutEmitter {
|
|
231
|
+
state: Cell<FlushState>,
|
|
232
|
+
layout_changed: Rc<PubSub<Vec<PanelId>>>,
|
|
233
|
+
active_changed: Rc<PubSub<Option<PanelId>>>,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
impl LayoutEmitter {
|
|
237
|
+
fn mark(&self, layout: bool, active: bool, workspace: &Workspace) {
|
|
238
|
+
let (next, spawn) = self.state.get().mark(layout, active);
|
|
239
|
+
self.state.set(next);
|
|
240
|
+
if spawn {
|
|
241
|
+
let workspace = workspace.clone();
|
|
242
|
+
spawn_owned("workspace_layout_flush", async move {
|
|
243
|
+
workspace.effects().settle().await;
|
|
244
|
+
workspace.flush_layout_events();
|
|
245
|
+
Ok(())
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// The loaded-[`Client`]s registry and the default-client designation. The
|
|
252
|
+
/// invariant "the default is always a registered client" holds in this one
|
|
253
|
+
/// impl: every default assignment registers first, and removal clears both.
|
|
254
|
+
#[derive(Default)]
|
|
255
|
+
struct ClientRegistry {
|
|
256
|
+
clients: Vec<Client>,
|
|
257
|
+
default: Option<Client>,
|
|
258
|
+
client_registered: Rc<PubSub<Client>>,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
impl ClientRegistry {
|
|
262
|
+
/// Add `client` if a client with the same (globally unique) name isn't
|
|
263
|
+
/// already present, returning — for a genuinely-new client — the pubsub
|
|
264
|
+
/// for the caller to emit OUTSIDE its borrow.
|
|
265
|
+
fn register(&mut self, client: Client) -> Option<Rc<PubSub<Client>>> {
|
|
266
|
+
if self
|
|
267
|
+
.clients
|
|
268
|
+
.iter()
|
|
269
|
+
.any(|c| c.get_name() == client.get_name())
|
|
270
|
+
{
|
|
271
|
+
return None;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
self.clients.push(client);
|
|
275
|
+
Some(self.client_registered.clone())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Drop the client named `name`, clearing the default designation if it
|
|
279
|
+
/// referred to it.
|
|
280
|
+
fn remove(&mut self, name: &str) {
|
|
281
|
+
self.clients.retain(|c| c.get_name() != name);
|
|
282
|
+
if self.default.as_ref().is_some_and(|c| c.get_name() == name) {
|
|
283
|
+
self.default = None;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
194
288
|
/// The multi-panel model backing a single `<perspective-viewer>`. See the
|
|
195
289
|
/// module docs for the Phase 1 (single-panel) invariants.
|
|
196
290
|
#[derive(Clone)]
|
|
@@ -203,46 +297,39 @@ impl PartialEq for Workspace {
|
|
|
203
297
|
}
|
|
204
298
|
|
|
205
299
|
struct WorkspaceData {
|
|
206
|
-
/// Panels in insertion order
|
|
207
|
-
|
|
300
|
+
/// Panels in insertion order, each with its per-panel attributes
|
|
301
|
+
/// (placement phase, master role).
|
|
302
|
+
panels: Vec<PanelEntry>,
|
|
208
303
|
|
|
209
304
|
/// The currently active/selected panel (a live panel in `panels`).
|
|
210
305
|
active: Option<PanelId>,
|
|
211
306
|
|
|
212
|
-
/// The
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
/// Every [`Client`] ever loaded into this element (registration order.
|
|
216
|
-
clients: Vec<Client>,
|
|
307
|
+
/// The element-level global filters.
|
|
308
|
+
filters: GlobalFilterSet,
|
|
309
|
+
filters_changed: Rc<PubSub<()>>,
|
|
217
310
|
|
|
218
|
-
///
|
|
219
|
-
|
|
220
|
-
/// reactive table binding (`tasks::table_lifecycle`).
|
|
221
|
-
client_registered: Rc<PubSub<Client>>,
|
|
311
|
+
/// The loaded-clients registry + default designation.
|
|
312
|
+
clients: ClientRegistry,
|
|
222
313
|
|
|
223
314
|
/// Monotonic counter backing [`Workspace::generate_id`].
|
|
224
315
|
next_id: usize,
|
|
225
316
|
|
|
226
|
-
///
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
/// The element-level global filters.
|
|
230
|
-
filters: GlobalFilterSet,
|
|
231
|
-
filters_changed: Rc<PubSub<()>>,
|
|
317
|
+
/// The `load()` reservation slot (see [`Reservation`]).
|
|
318
|
+
reservation: Reservation,
|
|
232
319
|
|
|
233
|
-
/// A layout tree staged by
|
|
320
|
+
/// A layout tree staged by `restoreWorkspace`.
|
|
234
321
|
pending_layout: Option<crate::js::Layout>,
|
|
235
322
|
|
|
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
323
|
/// In-flight effects (public mutators + scheduled internal flows),
|
|
244
|
-
/// drained by `flush()
|
|
324
|
+
/// drained by `flush()`.
|
|
245
325
|
effects: EffectLedger,
|
|
326
|
+
|
|
327
|
+
/// Fires on every [`PanelPhase`] transition.
|
|
328
|
+
staged_changed: Rc<PubSub<()>>,
|
|
329
|
+
|
|
330
|
+
/// Coalesced `layout_changed`/`active_changed` delivery (see
|
|
331
|
+
/// [`LayoutEmitter`]).
|
|
332
|
+
emitter: LayoutEmitter,
|
|
246
333
|
}
|
|
247
334
|
|
|
248
335
|
impl Default for Workspace {
|
|
@@ -257,18 +344,15 @@ impl Workspace {
|
|
|
257
344
|
Self(Rc::new(RefCell::new(WorkspaceData {
|
|
258
345
|
panels: Vec::new(),
|
|
259
346
|
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
347
|
filters: GlobalFilterSet::default(),
|
|
266
348
|
filters_changed: Rc::new(PubSub::default()),
|
|
349
|
+
clients: ClientRegistry::default(),
|
|
350
|
+
next_id: 0,
|
|
351
|
+
reservation: Reservation::Idle,
|
|
267
352
|
pending_layout: None,
|
|
268
|
-
staged: HashSet::new(),
|
|
269
|
-
staged_changed: Rc::new(PubSub::default()),
|
|
270
|
-
reserved: None,
|
|
271
353
|
effects: EffectLedger::default(),
|
|
354
|
+
staged_changed: Rc::new(PubSub::default()),
|
|
355
|
+
emitter: LayoutEmitter::default(),
|
|
272
356
|
})))
|
|
273
357
|
}
|
|
274
358
|
|
|
@@ -277,8 +361,44 @@ impl Workspace {
|
|
|
277
361
|
self.0.borrow().effects.clone()
|
|
278
362
|
}
|
|
279
363
|
|
|
364
|
+
pub fn layout_changed(&self) -> Rc<PubSub<Vec<PanelId>>> {
|
|
365
|
+
self.0.borrow().emitter.layout_changed.clone()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
pub fn active_changed(&self) -> Rc<PubSub<Option<PanelId>>> {
|
|
369
|
+
self.0.borrow().emitter.active_changed.clone()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// Deliver whatever the queued flush recorded, outside any borrow — the
|
|
373
|
+
/// body of the task [`LayoutEmitter::mark`] spawns.
|
|
374
|
+
fn flush_layout_events(&self) {
|
|
375
|
+
let (state, panels, active_id, layout_pubsub, active_pubsub) = {
|
|
376
|
+
let data = self.0.borrow();
|
|
377
|
+
(
|
|
378
|
+
data.emitter.state.replace(FlushState::Idle),
|
|
379
|
+
data.panels
|
|
380
|
+
.iter()
|
|
381
|
+
.map(|p| p.panel.id.clone())
|
|
382
|
+
.collect::<Vec<_>>(),
|
|
383
|
+
data.active.clone(),
|
|
384
|
+
data.emitter.layout_changed.clone(),
|
|
385
|
+
data.emitter.active_changed.clone(),
|
|
386
|
+
)
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
if let FlushState::Queued { layout, active } = state {
|
|
390
|
+
if layout {
|
|
391
|
+
layout_pubsub.emit(panels);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if active {
|
|
395
|
+
active_pubsub.emit(active_id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
280
400
|
/// Stage a layout tree for `MainPanel` to apply at its next `rendered`
|
|
281
|
-
/// pass (see `WorkspaceData::pending_layout`).
|
|
401
|
+
/// pass (see [`WorkspaceData::pending_layout`]).
|
|
282
402
|
pub fn set_pending_layout(&self, layout: crate::js::Layout) {
|
|
283
403
|
self.0.borrow_mut().pending_layout = Some(layout);
|
|
284
404
|
}
|
|
@@ -294,38 +414,42 @@ impl Workspace {
|
|
|
294
414
|
self.0.borrow().staged_changed.clone()
|
|
295
415
|
}
|
|
296
416
|
|
|
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
417
|
/// Promote a staged panel toward layout insertion, returning whether it
|
|
310
418
|
/// was still staged — restore completion and the staging deadline RACE
|
|
311
419
|
/// to promote, and only the winner proceeds. Emits `staged_changed` —
|
|
312
|
-
/// outside the borrow — iff the
|
|
313
|
-
pub fn
|
|
314
|
-
let (
|
|
420
|
+
/// outside the borrow — iff the phase changed.
|
|
421
|
+
pub fn promote(&self, id: &PanelId) -> bool {
|
|
422
|
+
let (promoted, pubsub) = {
|
|
315
423
|
let mut data = self.0.borrow_mut();
|
|
316
|
-
|
|
424
|
+
let promoted = data
|
|
425
|
+
.panels
|
|
426
|
+
.iter_mut()
|
|
427
|
+
.find(|p| &p.panel.id == id)
|
|
428
|
+
.map(|entry| {
|
|
429
|
+
matches!(
|
|
430
|
+
std::mem::replace(&mut entry.phase, PanelPhase::Placed),
|
|
431
|
+
PanelPhase::Staging
|
|
432
|
+
)
|
|
433
|
+
})
|
|
434
|
+
.unwrap_or(false);
|
|
435
|
+
|
|
436
|
+
(promoted, data.staged_changed.clone())
|
|
317
437
|
};
|
|
318
438
|
|
|
319
|
-
if
|
|
439
|
+
if promoted {
|
|
320
440
|
pubsub.emit(());
|
|
321
441
|
}
|
|
322
442
|
|
|
323
|
-
|
|
443
|
+
promoted
|
|
324
444
|
}
|
|
325
445
|
|
|
326
446
|
/// Whether `id` is a staged (created, not yet layout-inserted) panel.
|
|
327
447
|
pub fn is_staged(&self, id: &PanelId) -> bool {
|
|
328
|
-
self.0
|
|
448
|
+
self.0
|
|
449
|
+
.borrow()
|
|
450
|
+
.panels
|
|
451
|
+
.iter()
|
|
452
|
+
.any(|p| &p.panel.id == id && p.phase == PanelPhase::Staging)
|
|
329
453
|
}
|
|
330
454
|
|
|
331
455
|
/// The EFFECTIVE element-level global filters (master/detail
|
|
@@ -358,10 +482,7 @@ impl Workspace {
|
|
|
358
482
|
payload
|
|
359
483
|
}
|
|
360
484
|
|
|
361
|
-
/// Replace the global filter set
|
|
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`.
|
|
485
|
+
/// Replace the global filter set.
|
|
365
486
|
pub fn set_global_filters(&self, filters: Vec<Filter>) {
|
|
366
487
|
self.mutate_filters(|s| (s.set_restored(filters), ()));
|
|
367
488
|
}
|
|
@@ -377,52 +498,61 @@ impl Workspace {
|
|
|
377
498
|
self.mutate_filters(|s| (s.set_contribution(id, Vec::new()), ()));
|
|
378
499
|
}
|
|
379
500
|
|
|
380
|
-
/// Remove the effective-set clause at `index
|
|
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`).
|
|
501
|
+
/// Remove the effective-set clause at `index`.
|
|
384
502
|
pub fn remove_global_filter(&self, index: usize) -> Vec<PanelId> {
|
|
385
503
|
self.mutate_filters(|s| s.remove_clause(index))
|
|
386
504
|
}
|
|
387
505
|
|
|
388
506
|
/// Drop the entire global filter set (the `GlobalFilterBar` "Clear" /
|
|
389
|
-
/// element `reset()`)
|
|
390
|
-
/// selection-state cleanup. Master ROLES are untouched — like the
|
|
391
|
-
/// layout, they are workspace structure, not filter state.
|
|
507
|
+
/// element `reset()`).
|
|
392
508
|
pub fn clear_global_filters(&self) -> Vec<PanelId> {
|
|
393
509
|
self.mutate_filters(GlobalFilterSet::clear)
|
|
394
510
|
}
|
|
395
511
|
|
|
396
|
-
/// The master (filter-source) panel ids
|
|
397
|
-
/// this, and a `HashSet`'s per-instance iteration order would make
|
|
398
|
-
/// consecutive `save()` outputs byte-unstable (cf. the `panels`
|
|
399
|
-
/// `BTreeMap`).
|
|
512
|
+
/// The master (filter-source) panel ids.
|
|
400
513
|
pub fn masters(&self) -> Vec<PanelId> {
|
|
401
|
-
let mut masters: Vec<_> = self
|
|
514
|
+
let mut masters: Vec<_> = self
|
|
515
|
+
.0
|
|
516
|
+
.borrow()
|
|
517
|
+
.panels
|
|
518
|
+
.iter()
|
|
519
|
+
.filter(|p| p.master)
|
|
520
|
+
.map(|p| p.panel.id.clone())
|
|
521
|
+
.collect();
|
|
522
|
+
|
|
402
523
|
masters.sort();
|
|
403
524
|
masters
|
|
404
525
|
}
|
|
405
526
|
|
|
406
|
-
/// Replace the master role set — the
|
|
407
|
-
/// (ids already remapped to the fresh panel ids).
|
|
527
|
+
/// Replace the master role set — the `restoreWorkspace` entry point
|
|
528
|
+
/// (ids already remapped to the fresh panel ids). Ids naming no panel are
|
|
529
|
+
/// dropped (the caller has already warned on them).
|
|
408
530
|
pub fn set_masters(&self, ids: Vec<PanelId>) {
|
|
409
|
-
self.0.borrow_mut()
|
|
531
|
+
let mut data = self.0.borrow_mut();
|
|
532
|
+
for entry in data.panels.iter_mut() {
|
|
533
|
+
entry.master = ids.contains(&entry.panel.id);
|
|
534
|
+
}
|
|
410
535
|
}
|
|
411
536
|
|
|
412
537
|
/// Whether `id` is a master (filter-source) panel.
|
|
413
538
|
pub fn is_master(&self, id: &PanelId) -> bool {
|
|
414
|
-
self.0
|
|
539
|
+
self.0
|
|
540
|
+
.borrow()
|
|
541
|
+
.panels
|
|
542
|
+
.iter()
|
|
543
|
+
.any(|p| &p.panel.id == id && p.master)
|
|
415
544
|
}
|
|
416
545
|
|
|
417
546
|
/// Toggle `id`'s master/detail role, returning the new state (`true` =
|
|
418
|
-
/// master).
|
|
547
|
+
/// master). `false` (no role) if `id` names no panel.
|
|
419
548
|
pub fn toggle_master(&self, id: &PanelId) -> bool {
|
|
420
549
|
let mut data = self.0.borrow_mut();
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
550
|
+
match data.panels.iter_mut().find(|p| &p.panel.id == id) {
|
|
551
|
+
Some(entry) => {
|
|
552
|
+
entry.master = !entry.master;
|
|
553
|
+
entry.master
|
|
554
|
+
},
|
|
555
|
+
None => false,
|
|
426
556
|
}
|
|
427
557
|
}
|
|
428
558
|
|
|
@@ -444,7 +574,10 @@ impl Workspace {
|
|
|
444
574
|
pub fn active_panel(&self) -> Option<Panel> {
|
|
445
575
|
let data = self.0.borrow();
|
|
446
576
|
let active = data.active.as_ref()?;
|
|
447
|
-
data.panels
|
|
577
|
+
data.panels
|
|
578
|
+
.iter()
|
|
579
|
+
.find(|p| &p.panel.id == active)
|
|
580
|
+
.map(|p| p.panel.clone())
|
|
448
581
|
}
|
|
449
582
|
|
|
450
583
|
/// The active panel's [`Session`], or `None` with zero panels.
|
|
@@ -459,7 +592,12 @@ impl Workspace {
|
|
|
459
592
|
|
|
460
593
|
/// Look up a [`Panel`] by id.
|
|
461
594
|
pub fn panel(&self, id: &PanelId) -> Option<Panel> {
|
|
462
|
-
self.0
|
|
595
|
+
self.0
|
|
596
|
+
.borrow()
|
|
597
|
+
.panels
|
|
598
|
+
.iter()
|
|
599
|
+
.find(|p| &p.panel.id == id)
|
|
600
|
+
.map(|p| p.panel.clone())
|
|
463
601
|
}
|
|
464
602
|
|
|
465
603
|
/// Resolve a panel by id, or the active panel when `id` is `None` — the
|
|
@@ -475,17 +613,23 @@ impl Workspace {
|
|
|
475
613
|
/// fan-out source (fan-outs collect per-panel results; they never
|
|
476
614
|
/// sequential-abort on one panel's error).
|
|
477
615
|
pub fn panels(&self) -> Vec<Panel> {
|
|
478
|
-
self.0
|
|
616
|
+
self.0
|
|
617
|
+
.borrow()
|
|
618
|
+
.panels
|
|
619
|
+
.iter()
|
|
620
|
+
.map(|p| p.panel.clone())
|
|
621
|
+
.collect()
|
|
479
622
|
}
|
|
480
623
|
|
|
481
624
|
/// The number of PLACED panels (panels minus staged) — the single
|
|
482
625
|
/// source for panel-count chrome (`single`/`multi`, closable,
|
|
483
626
|
/// draggable, `only-child`).
|
|
484
627
|
pub fn placed_count(&self) -> usize {
|
|
485
|
-
|
|
486
|
-
|
|
628
|
+
self.0
|
|
629
|
+
.borrow()
|
|
630
|
+
.panels
|
|
487
631
|
.iter()
|
|
488
|
-
.filter(|p|
|
|
632
|
+
.filter(|p| p.phase == PanelPhase::Placed)
|
|
489
633
|
.count()
|
|
490
634
|
}
|
|
491
635
|
|
|
@@ -495,7 +639,7 @@ impl Workspace {
|
|
|
495
639
|
.borrow()
|
|
496
640
|
.panels
|
|
497
641
|
.iter()
|
|
498
|
-
.map(|p| p.id.clone())
|
|
642
|
+
.map(|p| p.panel.id.clone())
|
|
499
643
|
.collect()
|
|
500
644
|
}
|
|
501
645
|
|
|
@@ -509,50 +653,119 @@ impl Workspace {
|
|
|
509
653
|
self.0.borrow().panels.is_empty()
|
|
510
654
|
}
|
|
511
655
|
|
|
512
|
-
/// Append a [`Panel`]
|
|
513
|
-
/// becomes the active one (there is no other candidate).
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
656
|
+
/// Append a [`Panel`] at `phase`. When the element had zero panels, the
|
|
657
|
+
/// inserted panel becomes the active one (there is no other candidate).
|
|
658
|
+
/// A [`PanelPhase::Staging`] insert emits `staged_changed` — outside the
|
|
659
|
+
/// borrow — so the phase and its announcement are one operation.
|
|
660
|
+
pub fn insert_panel(&self, panel: Panel, phase: PanelPhase) {
|
|
661
|
+
let staged_pubsub = {
|
|
662
|
+
let mut data = self.0.borrow_mut();
|
|
663
|
+
let activated = if data.active.is_none() {
|
|
664
|
+
data.active = Some(panel.id.clone());
|
|
665
|
+
true
|
|
666
|
+
} else {
|
|
667
|
+
false
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
panel
|
|
671
|
+
.renderer
|
|
672
|
+
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
673
|
+
data.panels.push(PanelEntry {
|
|
674
|
+
panel,
|
|
675
|
+
phase,
|
|
676
|
+
master: false,
|
|
677
|
+
});
|
|
519
678
|
|
|
520
|
-
|
|
521
|
-
.
|
|
522
|
-
.
|
|
523
|
-
|
|
524
|
-
|
|
679
|
+
Self::sync_solo_flags(&data);
|
|
680
|
+
data.emitter.mark(true, activated, self);
|
|
681
|
+
(phase == PanelPhase::Staging).then(|| data.staged_changed.clone())
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
if let Some(pubsub) = staged_pubsub {
|
|
685
|
+
pubsub.emit(());
|
|
686
|
+
}
|
|
525
687
|
}
|
|
526
688
|
|
|
527
|
-
/// Hold `panel` in the reservation slot (
|
|
689
|
+
/// Hold `panel` in the reservation slot ([`Reservation::Pending`]):
|
|
528
690
|
/// NOT placed, invisible to every placed-panel consumer, awaiting a
|
|
529
691
|
/// pending `load()`'s payload classification or an interim claimant.
|
|
692
|
+
/// Resets any prior claim record — the slot serves one `load()` cycle at
|
|
693
|
+
/// a time.
|
|
530
694
|
pub fn reserve_panel(&self, panel: Panel) {
|
|
531
|
-
self.0.borrow_mut()
|
|
695
|
+
let mut data = self.0.borrow_mut();
|
|
696
|
+
debug_assert!(
|
|
697
|
+
!matches!(data.reservation, Reservation::Pending(_)),
|
|
698
|
+
"reservation overwritten while pending — adopt via `reserved_panel` first"
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
data.reservation = Reservation::Pending(panel);
|
|
532
702
|
}
|
|
533
703
|
|
|
534
704
|
/// The reservation slot's current occupant (shared handles), without a
|
|
535
705
|
/// transfer — a second `load()` on a still-empty element adopts the same
|
|
536
706
|
/// reservation rather than creating a competing one.
|
|
537
707
|
pub fn reserved_panel(&self) -> Option<Panel> {
|
|
538
|
-
self.0.borrow().
|
|
708
|
+
match &self.0.borrow().reservation {
|
|
709
|
+
Reservation::Pending(panel) => Some(panel.clone()),
|
|
710
|
+
_ => None,
|
|
711
|
+
}
|
|
539
712
|
}
|
|
540
713
|
|
|
541
|
-
///
|
|
714
|
+
/// CLAIM the reserved panel: transfer it out of the reservation slot into
|
|
542
715
|
/// the placed set ([`Self::insert_panel`] — auto-activating on an empty
|
|
543
|
-
/// element)
|
|
544
|
-
///
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
716
|
+
/// element), recording `has_table` — whether the claimant carried a
|
|
717
|
+
/// `table` — atomically with the transfer ([`Reservation::Claimed`]).
|
|
718
|
+
/// `None` when nothing is pending (no reservation, or the other actor
|
|
719
|
+
/// transferred first).
|
|
720
|
+
pub fn claim_reserved(&self, has_table: bool) -> Option<Panel> {
|
|
721
|
+
let panel = {
|
|
722
|
+
let mut data = self.0.borrow_mut();
|
|
723
|
+
match std::mem::take(&mut data.reservation) {
|
|
724
|
+
Reservation::Pending(panel) => {
|
|
725
|
+
data.reservation = Reservation::Claimed { has_table };
|
|
726
|
+
Some(panel)
|
|
727
|
+
},
|
|
728
|
+
other => {
|
|
729
|
+
data.reservation = other;
|
|
730
|
+
None
|
|
731
|
+
},
|
|
732
|
+
}
|
|
733
|
+
}?;
|
|
734
|
+
|
|
735
|
+
self.insert_panel(panel.clone(), PanelPhase::Placed);
|
|
548
736
|
Some(panel)
|
|
549
737
|
}
|
|
550
738
|
|
|
551
739
|
/// DISCARD the reserved panel: transfer it out of the reservation slot
|
|
552
740
|
/// WITHOUT placing it, for disposal (an inert `Client` payload with no
|
|
553
|
-
/// claimant, or teardown draining). `None` when
|
|
741
|
+
/// claimant, or teardown draining). `None` when nothing is pending; a
|
|
742
|
+
/// claim record is left intact for [`Self::resolve_claim`].
|
|
554
743
|
pub fn take_reserved(&self) -> Option<Panel> {
|
|
555
|
-
self.0.borrow_mut()
|
|
744
|
+
let mut data = self.0.borrow_mut();
|
|
745
|
+
match std::mem::take(&mut data.reservation) {
|
|
746
|
+
Reservation::Pending(panel) => Some(panel),
|
|
747
|
+
other => {
|
|
748
|
+
data.reservation = other;
|
|
749
|
+
None
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/// ONE-SHOT read of the claim record: `Some(has_table)` if the
|
|
755
|
+
/// reservation was claimed ([`Reservation::Claimed`]) — resetting the
|
|
756
|
+
/// slot to [`Reservation::Idle`] — or `None` if no claim happened.
|
|
757
|
+
/// Consuming closes the stale-flag window the former boolean had —
|
|
758
|
+
/// `load()`'s inert-`Client` epilogue is the only reader, and reads it
|
|
759
|
+
/// exactly once (eviction arms on `Some(false)`).
|
|
760
|
+
pub fn resolve_claim(&self) -> Option<bool> {
|
|
761
|
+
let mut data = self.0.borrow_mut();
|
|
762
|
+
match data.reservation {
|
|
763
|
+
Reservation::Claimed { has_table } => {
|
|
764
|
+
data.reservation = Reservation::Idle;
|
|
765
|
+
Some(has_table)
|
|
766
|
+
},
|
|
767
|
+
_ => None,
|
|
768
|
+
}
|
|
556
769
|
}
|
|
557
770
|
|
|
558
771
|
/// Sync every panel renderer's solo (lone-panel) flag with the current
|
|
@@ -561,47 +774,49 @@ impl Workspace {
|
|
|
561
774
|
/// locked plugin dispatch — see `Renderer::stamp_active`).
|
|
562
775
|
fn sync_solo_flags(data: &WorkspaceData) {
|
|
563
776
|
let is_solo = data.panels.len() == 1;
|
|
564
|
-
for
|
|
565
|
-
panel.renderer.set_solo_flag(is_solo);
|
|
777
|
+
for entry in data.panels.iter() {
|
|
778
|
+
entry.panel.renderer.set_solo_flag(is_solo);
|
|
566
779
|
}
|
|
567
780
|
}
|
|
568
781
|
|
|
569
|
-
/// Remove a [`Panel`] by id, returning it if present.
|
|
570
|
-
///
|
|
571
|
-
///
|
|
572
|
-
///
|
|
573
|
-
///
|
|
782
|
+
/// Remove a [`Panel`] by id, returning it if present. The entry's
|
|
783
|
+
/// attributes (phase, master role) leave with it structurally; its
|
|
784
|
+
/// global-filter contribution is dropped here, so neither can outlive
|
|
785
|
+
/// the panel on ANY removal path (close, `restoreWorkspace`'s batch
|
|
786
|
+
/// replacement).
|
|
574
787
|
pub fn remove_panel(&self, id: &PanelId) -> Option<Panel> {
|
|
575
|
-
let (removed, changed,
|
|
788
|
+
let (removed, changed, filters_pubsub, was_staged, staged_pubsub) = {
|
|
576
789
|
let mut data = self.0.borrow_mut();
|
|
577
|
-
data.masters.remove(id);
|
|
578
|
-
let staged_removed = data.staged.remove(id);
|
|
579
790
|
let changed = data.filters.set_contribution(id, Vec::new());
|
|
580
|
-
let removed = data
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
791
|
+
let (removed, was_staged) = match data.panels.iter().position(|p| &p.panel.id == id) {
|
|
792
|
+
Some(idx) => {
|
|
793
|
+
let entry = data.panels.remove(idx);
|
|
794
|
+
(Some(entry.panel), entry.phase == PanelPhase::Staging)
|
|
795
|
+
},
|
|
796
|
+
None => (None, false),
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
let deactivated = data.active.as_ref() == Some(id);
|
|
800
|
+
if deactivated {
|
|
587
801
|
data.active = None;
|
|
588
802
|
}
|
|
589
803
|
|
|
590
804
|
Self::sync_solo_flags(&data);
|
|
805
|
+
data.emitter.mark(removed.is_some(), deactivated, self);
|
|
591
806
|
(
|
|
592
807
|
removed,
|
|
593
808
|
changed,
|
|
594
809
|
data.filters_changed.clone(),
|
|
595
|
-
|
|
810
|
+
was_staged,
|
|
596
811
|
data.staged_changed.clone(),
|
|
597
812
|
)
|
|
598
813
|
};
|
|
599
814
|
|
|
600
815
|
if changed {
|
|
601
|
-
|
|
816
|
+
filters_pubsub.emit(());
|
|
602
817
|
}
|
|
603
818
|
|
|
604
|
-
if
|
|
819
|
+
if was_staged {
|
|
605
820
|
staged_pubsub.emit(());
|
|
606
821
|
}
|
|
607
822
|
|
|
@@ -612,14 +827,17 @@ impl Workspace {
|
|
|
612
827
|
/// panel.
|
|
613
828
|
pub fn set_active(&self, id: PanelId) -> bool {
|
|
614
829
|
let mut data = self.0.borrow_mut();
|
|
615
|
-
if data.panels.iter().any(|p| p.id == id) {
|
|
830
|
+
if data.panels.iter().any(|p| p.panel.id == id) {
|
|
831
|
+
let changed = data.active.as_ref() != Some(&id);
|
|
616
832
|
data.active = Some(id);
|
|
617
|
-
for
|
|
618
|
-
|
|
833
|
+
for entry in data.panels.iter() {
|
|
834
|
+
entry
|
|
835
|
+
.panel
|
|
619
836
|
.renderer
|
|
620
|
-
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
837
|
+
.set_active_flag(data.active.as_ref() == Some(&entry.panel.id));
|
|
621
838
|
}
|
|
622
839
|
|
|
840
|
+
data.emitter.mark(false, changed, self);
|
|
623
841
|
true
|
|
624
842
|
} else {
|
|
625
843
|
false
|
|
@@ -628,7 +846,7 @@ impl Workspace {
|
|
|
628
846
|
|
|
629
847
|
/// The default [`Client`], if one has been loaded.
|
|
630
848
|
pub fn default_client(&self) -> Option<Client> {
|
|
631
|
-
self.0.borrow().
|
|
849
|
+
self.0.borrow().clients.default.clone()
|
|
632
850
|
}
|
|
633
851
|
|
|
634
852
|
/// The active panel's bound [`Client`], if any — the default target of a
|
|
@@ -644,8 +862,13 @@ impl Workspace {
|
|
|
644
862
|
.borrow()
|
|
645
863
|
.panels
|
|
646
864
|
.iter()
|
|
647
|
-
.filter(|p|
|
|
648
|
-
|
|
865
|
+
.filter(|p| {
|
|
866
|
+
p.panel
|
|
867
|
+
.session
|
|
868
|
+
.get_client()
|
|
869
|
+
.is_some_and(|c| c.get_name() == name)
|
|
870
|
+
})
|
|
871
|
+
.map(|p| p.panel.id.clone())
|
|
649
872
|
.collect()
|
|
650
873
|
}
|
|
651
874
|
|
|
@@ -655,26 +878,27 @@ impl Workspace {
|
|
|
655
878
|
/// [`Workspace::panels_for_client`]) — `clients()` unions in live panel
|
|
656
879
|
/// sessions, so a lingering panel would resurrect it.
|
|
657
880
|
pub fn remove_client(&self, name: &str) {
|
|
658
|
-
|
|
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
|
-
}
|
|
881
|
+
self.0.borrow_mut().clients.remove(name);
|
|
667
882
|
}
|
|
668
883
|
|
|
669
884
|
/// Record the default [`Client`] if not already set (first-wins, matching
|
|
670
885
|
/// the "first `Client` passed to `load()` is the default" rule). Always
|
|
671
886
|
/// registers the client (see [`Workspace::register_client`]) — first-wins
|
|
672
|
-
/// applies only to the *default* designation
|
|
887
|
+
/// applies only to the *default* designation, which is assigned BEFORE
|
|
888
|
+
/// the registration event fires, so listeners observe both together.
|
|
673
889
|
pub fn set_default_client(&self, client: Client) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
data.
|
|
890
|
+
let pubsub = {
|
|
891
|
+
let mut data = self.0.borrow_mut();
|
|
892
|
+
let pubsub = data.clients.register(client.clone());
|
|
893
|
+
if data.clients.default.is_none() {
|
|
894
|
+
data.clients.default = Some(client.clone());
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
pubsub
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
if let Some(pubsub) = pubsub {
|
|
901
|
+
pubsub.emit(client);
|
|
678
902
|
}
|
|
679
903
|
}
|
|
680
904
|
|
|
@@ -682,27 +906,15 @@ impl Workspace {
|
|
|
682
906
|
/// same (globally unique) name isn't already present. Emits
|
|
683
907
|
/// `client_registered` — outside the borrow — for a genuinely-new client.
|
|
684
908
|
pub fn register_client(&self, client: Client) {
|
|
685
|
-
let pubsub =
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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);
|
|
909
|
+
let pubsub = self.0.borrow_mut().clients.register(client.clone());
|
|
910
|
+
if let Some(pubsub) = pubsub {
|
|
911
|
+
pubsub.emit(client);
|
|
912
|
+
}
|
|
700
913
|
}
|
|
701
914
|
|
|
702
|
-
/// A handle to the `client_registered` PubSub (see
|
|
703
|
-
/// [`WorkspaceData::client_registered`]).
|
|
915
|
+
/// A handle to the `client_registered` PubSub (see [`ClientRegistry`]).
|
|
704
916
|
pub fn client_registered(&self) -> Rc<PubSub<Client>> {
|
|
705
|
-
self.0.borrow().client_registered.clone()
|
|
917
|
+
self.0.borrow().clients.client_registered.clone()
|
|
706
918
|
}
|
|
707
919
|
|
|
708
920
|
/// All loaded [`Client`]s: the registry, unioned with every panel
|
|
@@ -710,9 +922,9 @@ impl Workspace {
|
|
|
710
922
|
/// bypasses registration), deduped by name in registration order.
|
|
711
923
|
pub fn clients(&self) -> Vec<Client> {
|
|
712
924
|
let data = self.0.borrow();
|
|
713
|
-
let mut clients = data.clients.clone();
|
|
714
|
-
for
|
|
715
|
-
if let Some(client) = panel.session.get_client()
|
|
925
|
+
let mut clients = data.clients.clients.clone();
|
|
926
|
+
for entry in &data.panels {
|
|
927
|
+
if let Some(client) = entry.panel.session.get_client()
|
|
716
928
|
&& !clients.iter().any(|c| c.get_name() == client.get_name())
|
|
717
929
|
{
|
|
718
930
|
clients.push(client);
|
|
@@ -843,4 +1055,47 @@ mod tests {
|
|
|
843
1055
|
assert_eq!(s.flatten(), Vec::<Filter>::new());
|
|
844
1056
|
assert_eq!(s.clear(), (false, Vec::new()));
|
|
845
1057
|
}
|
|
1058
|
+
|
|
1059
|
+
#[test]
|
|
1060
|
+
fn flush_state_spawns_exactly_once_per_cycle() {
|
|
1061
|
+
let (state, spawn) = FlushState::Idle.mark(true, false);
|
|
1062
|
+
assert!(spawn);
|
|
1063
|
+
assert_eq!(state, FlushState::Queued {
|
|
1064
|
+
layout: true,
|
|
1065
|
+
active: false
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Subsequent marks merge without re-spawning.
|
|
1069
|
+
let (state, spawn) = state.mark(false, true);
|
|
1070
|
+
assert!(!spawn);
|
|
1071
|
+
assert_eq!(state, FlushState::Queued {
|
|
1072
|
+
layout: true,
|
|
1073
|
+
active: true
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
let (state, spawn) = state.mark(true, true);
|
|
1077
|
+
assert!(!spawn);
|
|
1078
|
+
assert_eq!(state, FlushState::Queued {
|
|
1079
|
+
layout: true,
|
|
1080
|
+
active: true
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
#[test]
|
|
1085
|
+
fn flush_state_ignores_empty_marks() {
|
|
1086
|
+
// A no-event mark neither queues nor spawns — `Queued{false,false}`
|
|
1087
|
+
// is unconstructible through `mark`.
|
|
1088
|
+
let (state, spawn) = FlushState::Idle.mark(false, false);
|
|
1089
|
+
assert!(!spawn);
|
|
1090
|
+
assert_eq!(state, FlushState::Idle);
|
|
1091
|
+
|
|
1092
|
+
let queued = FlushState::Queued {
|
|
1093
|
+
layout: true,
|
|
1094
|
+
active: false,
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
let (state, spawn) = queued.mark(false, false);
|
|
1098
|
+
assert!(!spawn);
|
|
1099
|
+
assert_eq!(state, queued);
|
|
1100
|
+
}
|
|
846
1101
|
}
|