@perspective-dev/viewer 5.2.0 → 5.3.1
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 +8 -8
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -0
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -0
- 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/ledger.css +1 -0
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.css +1 -0
- 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/css/velvet.css +1 -0
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.d.ts +16 -3
- package/dist/esm/perspective-viewer.d.ts +1 -0
- package/dist/esm/perspective-viewer.inline.js +9 -9
- package/dist/esm/perspective-viewer.inline.js.map +3 -3
- package/dist/esm/perspective-viewer.js +8 -8
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +10 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +1 -1
- package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +1 -1
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +13 -3
- package/dist/wasm/perspective-viewer.d.ts +43 -36
- package/dist/wasm/perspective-viewer.js +198 -136
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +19 -17
- package/package.json +1 -1
- package/src/css/column-settings-panel.css +28 -5
- package/src/css/column-style.css +291 -31
- package/src/css/containers/tabs.css +13 -3
- package/src/css/dom/select.css +21 -9
- package/src/css/form/code-editor.css +4 -4
- package/src/css/plugin-selector.css +1 -0
- package/src/css/plugin-settings-panel.css +72 -0
- package/src/css/viewer.css +0 -5
- package/src/rust/agent/tools.rs +53 -19
- 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 +8 -0
- 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 +48 -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 +330 -234
- package/src/rust/components/column_settings_sidebar.rs +88 -69
- package/src/rust/components/containers/control_group.rs +91 -0
- package/src/rust/components/containers/mod.rs +1 -0
- package/src/rust/components/containers/select.rs +8 -2
- package/src/rust/components/datetime_column_style/simple.rs +15 -4
- package/src/rust/components/datetime_column_style.rs +50 -10
- 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 +16 -6
- package/src/rust/components/form/debug.rs +1 -0
- package/src/rust/components/form/mod.rs +3 -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/plugin_tab.rs +126 -112
- package/src/rust/components/settings_panel.rs +26 -20
- package/src/rust/components/status_bar.rs +0 -1
- package/src/rust/components/style_controls/number_string_format/digits_section.rs +22 -10
- package/src/rust/components/style_controls/number_string_format/misc_section.rs +6 -2
- package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -0
- package/src/rust/components/style_controls/number_string_format/types.rs +24 -0
- package/src/rust/components/style_controls/number_string_format.rs +37 -14
- package/src/rust/components/viewer/msg.rs +46 -7
- package/src/rust/components/viewer/panels.rs +11 -9
- package/src/rust/components/viewer/render.rs +8 -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/config/column_config_schema.rs +435 -45
- package/src/rust/config/css_scale.rs +882 -0
- package/src/rust/config/datetime_column_style.rs +4 -21
- package/src/rust/config/mod.rs +2 -0
- package/src/rust/config/number_string_format.rs +211 -18
- package/src/rust/config/options.rs +10 -0
- package/src/rust/config/viewer_config.rs +49 -55
- package/src/rust/config/workspace_config.rs +132 -5
- package/src/rust/custom_elements/viewer.rs +214 -155
- package/src/rust/custom_events.rs +14 -1
- package/src/rust/js/regular_layout.rs +3 -3
- package/src/rust/lib.rs +2 -0
- package/src/rust/presentation/column_locator.rs +19 -5
- package/src/rust/presentation.rs +209 -36
- package/src/rust/queries/column_locator.rs +74 -44
- package/src/rust/queries/get_viewer_config.rs +0 -3
- 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 +50 -12
- package/src/rust/renderer/plugin_config.rs +362 -53
- package/src/rust/renderer.rs +15 -42
- package/src/rust/session/column_defaults_update.rs +15 -8
- package/src/rust/session/metadata.rs +5 -1
- package/src/rust/session/props.rs +1 -0
- package/src/rust/session.rs +9 -6
- package/src/rust/tasks/create_panel.rs +44 -57
- package/src/rust/tasks/edit_expression.rs +19 -23
- package/src/rust/tasks/edit_window.rs +8 -10
- 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 +11 -13
- package/src/rust/tasks/set_edit_mode.rs +3 -1
- package/src/rust/tasks/sync_update_panels.rs +11 -1
- 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 +435 -272
- package/src/svg/lock.svg +7 -0
- package/src/themes/blueprint.css +190 -0
- package/src/themes/botanical.css +14 -17
- package/src/themes/defaults.css +4 -110
- package/src/themes/dracula.css +19 -14
- package/src/themes/eggplant.css +189 -0
- package/src/themes/gruvbox-dark.css +23 -15
- package/src/themes/gruvbox.css +11 -1
- package/src/themes/icons.css +2 -2
- package/src/themes/intl/de.css +23 -1
- package/src/themes/intl/es.css +23 -1
- package/src/themes/intl/fr.css +23 -1
- package/src/themes/intl/ja.css +23 -1
- package/src/themes/intl/pt.css +23 -1
- package/src/themes/intl/zh.css +23 -1
- package/src/themes/intl.css +26 -4
- package/src/themes/ledger.css +195 -0
- package/src/themes/monokai.css +24 -14
- package/src/themes/nord.css +186 -0
- package/src/themes/phosphor.css +7 -14
- package/src/themes/pro-dark.css +12 -16
- package/src/themes/pro.css +118 -0
- package/src/themes/solarized-dark.css +9 -16
- package/src/themes/solarized.css +7 -0
- package/src/themes/themes.css +10 -5
- package/src/themes/vaporwave.css +22 -14
- package/src/themes/velvet.css +192 -0
- package/src/ts/column-format.ts +52 -2
- package/src/ts/perspective-viewer.ts +1 -0
- package/src/ts/plugin.ts +14 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +1 -1
- package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +1 -1
- package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +11 -3
- package/src/rust/components/form/color_range_selector.rs +0 -137
- package/src/svg/dropdown-selector-light.svg +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-02e461f3d7062b26 → perspective-viewer-87bd21b215d0631a}/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;
|
|
@@ -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,62 +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
|
-
|
|
317
|
+
/// The `load()` reservation slot (see [`Reservation`]).
|
|
318
|
+
reservation: Reservation,
|
|
228
319
|
|
|
229
|
-
///
|
|
230
|
-
filters: GlobalFilterSet,
|
|
231
|
-
filters_changed: Rc<PubSub<()>>,
|
|
232
|
-
|
|
233
|
-
/// A layout tree staged by whole-element `restore`.
|
|
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,
|
|
246
326
|
|
|
247
|
-
///
|
|
248
|
-
|
|
249
|
-
/// [`Workspace::schedule_layout_flush`]).
|
|
250
|
-
layout_dirty: bool,
|
|
251
|
-
layout_changed: Rc<PubSub<Vec<PanelId>>>,
|
|
252
|
-
|
|
253
|
-
/// The active panel changed since the last emit. A separate channel from
|
|
254
|
-
/// `layout_dirty`: "which panel is selected" and "which panels exist" are
|
|
255
|
-
/// distinct facts, and one event may not mean both.
|
|
256
|
-
active_dirty: bool,
|
|
257
|
-
active_changed: Rc<PubSub<Option<PanelId>>>,
|
|
327
|
+
/// Fires on every [`PanelPhase`] transition.
|
|
328
|
+
staged_changed: Rc<PubSub<()>>,
|
|
258
329
|
|
|
259
|
-
///
|
|
260
|
-
///
|
|
261
|
-
|
|
330
|
+
/// Coalesced `layout_changed`/`active_changed` delivery (see
|
|
331
|
+
/// [`LayoutEmitter`]).
|
|
332
|
+
emitter: LayoutEmitter,
|
|
262
333
|
}
|
|
263
334
|
|
|
264
335
|
impl Default for Workspace {
|
|
@@ -273,23 +344,15 @@ impl Workspace {
|
|
|
273
344
|
Self(Rc::new(RefCell::new(WorkspaceData {
|
|
274
345
|
panels: Vec::new(),
|
|
275
346
|
active: None,
|
|
276
|
-
default_client: None,
|
|
277
|
-
clients: Vec::new(),
|
|
278
|
-
client_registered: Rc::new(PubSub::default()),
|
|
279
|
-
next_id: 0,
|
|
280
|
-
masters: HashSet::new(),
|
|
281
347
|
filters: GlobalFilterSet::default(),
|
|
282
348
|
filters_changed: Rc::new(PubSub::default()),
|
|
349
|
+
clients: ClientRegistry::default(),
|
|
350
|
+
next_id: 0,
|
|
351
|
+
reservation: Reservation::Idle,
|
|
283
352
|
pending_layout: None,
|
|
284
|
-
staged: HashSet::new(),
|
|
285
|
-
staged_changed: Rc::new(PubSub::default()),
|
|
286
|
-
reserved: None,
|
|
287
353
|
effects: EffectLedger::default(),
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
active_dirty: false,
|
|
291
|
-
active_changed: Rc::new(PubSub::default()),
|
|
292
|
-
flush_scheduled: false,
|
|
354
|
+
staged_changed: Rc::new(PubSub::default()),
|
|
355
|
+
emitter: LayoutEmitter::default(),
|
|
293
356
|
})))
|
|
294
357
|
}
|
|
295
358
|
|
|
@@ -299,63 +362,43 @@ impl Workspace {
|
|
|
299
362
|
}
|
|
300
363
|
|
|
301
364
|
pub fn layout_changed(&self) -> Rc<PubSub<Vec<PanelId>>> {
|
|
302
|
-
self.0.borrow().layout_changed.clone()
|
|
365
|
+
self.0.borrow().emitter.layout_changed.clone()
|
|
303
366
|
}
|
|
304
367
|
|
|
305
368
|
pub fn active_changed(&self) -> Rc<PubSub<Option<PanelId>>> {
|
|
306
|
-
self.0.borrow().active_changed.clone()
|
|
369
|
+
self.0.borrow().emitter.active_changed.clone()
|
|
307
370
|
}
|
|
308
371
|
|
|
309
|
-
///
|
|
310
|
-
///
|
|
311
|
-
fn schedule_layout_flush(&self) {
|
|
312
|
-
let schedule = {
|
|
313
|
-
let mut data = self.0.borrow_mut();
|
|
314
|
-
let dirty = data.layout_dirty || data.active_dirty;
|
|
315
|
-
let queued = data.flush_scheduled;
|
|
316
|
-
data.flush_scheduled |= dirty;
|
|
317
|
-
dirty && !queued
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
if !schedule {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
let effects = self.effects();
|
|
325
|
-
let this = self.clone();
|
|
326
|
-
spawn_owned("workspace_layout_flush", async move {
|
|
327
|
-
effects.settle().await;
|
|
328
|
-
this.flush_layout_events();
|
|
329
|
-
Ok(())
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/// Emit whatever is dirty, outside any borrow.
|
|
372
|
+
/// Deliver whatever the queued flush recorded, outside any borrow — the
|
|
373
|
+
/// body of the task [`LayoutEmitter::mark`] spawns.
|
|
334
374
|
fn flush_layout_events(&self) {
|
|
335
|
-
let (
|
|
336
|
-
let
|
|
337
|
-
data.flush_scheduled = false;
|
|
375
|
+
let (state, panels, active_id, layout_pubsub, active_pubsub) = {
|
|
376
|
+
let data = self.0.borrow();
|
|
338
377
|
(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
378
|
+
data.emitter.state.replace(FlushState::Idle),
|
|
379
|
+
data.panels
|
|
380
|
+
.iter()
|
|
381
|
+
.map(|p| p.panel.id.clone())
|
|
382
|
+
.collect::<Vec<_>>(),
|
|
342
383
|
data.active.clone(),
|
|
343
|
-
data.layout_changed.clone(),
|
|
344
|
-
data.active_changed.clone(),
|
|
384
|
+
data.emitter.layout_changed.clone(),
|
|
385
|
+
data.emitter.active_changed.clone(),
|
|
345
386
|
)
|
|
346
387
|
};
|
|
347
388
|
|
|
348
|
-
if layout {
|
|
349
|
-
|
|
350
|
-
|
|
389
|
+
if let FlushState::Queued { layout, active } = state {
|
|
390
|
+
if layout {
|
|
391
|
+
layout_pubsub.emit(panels);
|
|
392
|
+
}
|
|
351
393
|
|
|
352
|
-
|
|
353
|
-
|
|
394
|
+
if active {
|
|
395
|
+
active_pubsub.emit(active_id);
|
|
396
|
+
}
|
|
354
397
|
}
|
|
355
398
|
}
|
|
356
399
|
|
|
357
400
|
/// Stage a layout tree for `MainPanel` to apply at its next `rendered`
|
|
358
|
-
/// pass (see `WorkspaceData::pending_layout`).
|
|
401
|
+
/// pass (see [`WorkspaceData::pending_layout`]).
|
|
359
402
|
pub fn set_pending_layout(&self, layout: crate::js::Layout) {
|
|
360
403
|
self.0.borrow_mut().pending_layout = Some(layout);
|
|
361
404
|
}
|
|
@@ -371,38 +414,42 @@ impl Workspace {
|
|
|
371
414
|
self.0.borrow().staged_changed.clone()
|
|
372
415
|
}
|
|
373
416
|
|
|
374
|
-
/// Mark a freshly-created panel STAGED (see [`WorkspaceData::staged`]).
|
|
375
|
-
/// Emits `staged_changed` — outside the borrow.
|
|
376
|
-
pub fn stage_panel(&self, id: &PanelId) {
|
|
377
|
-
let pubsub = {
|
|
378
|
-
let mut data = self.0.borrow_mut();
|
|
379
|
-
data.staged.insert(id.clone());
|
|
380
|
-
data.staged_changed.clone()
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
pubsub.emit(());
|
|
384
|
-
}
|
|
385
|
-
|
|
386
417
|
/// Promote a staged panel toward layout insertion, returning whether it
|
|
387
418
|
/// was still staged — restore completion and the staging deadline RACE
|
|
388
419
|
/// to promote, and only the winner proceeds. Emits `staged_changed` —
|
|
389
|
-
/// outside the borrow — iff the
|
|
390
|
-
pub fn
|
|
391
|
-
let (
|
|
420
|
+
/// outside the borrow — iff the phase changed.
|
|
421
|
+
pub fn promote(&self, id: &PanelId) -> bool {
|
|
422
|
+
let (promoted, pubsub) = {
|
|
392
423
|
let mut data = self.0.borrow_mut();
|
|
393
|
-
|
|
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())
|
|
394
437
|
};
|
|
395
438
|
|
|
396
|
-
if
|
|
439
|
+
if promoted {
|
|
397
440
|
pubsub.emit(());
|
|
398
441
|
}
|
|
399
442
|
|
|
400
|
-
|
|
443
|
+
promoted
|
|
401
444
|
}
|
|
402
445
|
|
|
403
446
|
/// Whether `id` is a staged (created, not yet layout-inserted) panel.
|
|
404
447
|
pub fn is_staged(&self, id: &PanelId) -> bool {
|
|
405
|
-
self.0
|
|
448
|
+
self.0
|
|
449
|
+
.borrow()
|
|
450
|
+
.panels
|
|
451
|
+
.iter()
|
|
452
|
+
.any(|p| &p.panel.id == id && p.phase == PanelPhase::Staging)
|
|
406
453
|
}
|
|
407
454
|
|
|
408
455
|
/// The EFFECTIVE element-level global filters (master/detail
|
|
@@ -435,10 +482,7 @@ impl Workspace {
|
|
|
435
482
|
payload
|
|
436
483
|
}
|
|
437
484
|
|
|
438
|
-
/// Replace the global filter set
|
|
439
|
-
/// dropping every master contribution — the whole-element `restore`
|
|
440
|
-
/// entry point. Callers push the new set into panel sessions via
|
|
441
|
-
/// `tasks::apply_global_filters`.
|
|
485
|
+
/// Replace the global filter set.
|
|
442
486
|
pub fn set_global_filters(&self, filters: Vec<Filter>) {
|
|
443
487
|
self.mutate_filters(|s| (s.set_restored(filters), ()));
|
|
444
488
|
}
|
|
@@ -454,52 +498,61 @@ impl Workspace {
|
|
|
454
498
|
self.mutate_filters(|s| (s.set_contribution(id, Vec::new()), ()));
|
|
455
499
|
}
|
|
456
500
|
|
|
457
|
-
/// Remove the effective-set clause at `index
|
|
458
|
-
/// chip ×), returning the OWNING master panels of the removed clause so
|
|
459
|
-
/// the caller can clear their selection state
|
|
460
|
-
/// (`tasks::clear_master_selections`).
|
|
501
|
+
/// Remove the effective-set clause at `index`.
|
|
461
502
|
pub fn remove_global_filter(&self, index: usize) -> Vec<PanelId> {
|
|
462
503
|
self.mutate_filters(|s| s.remove_clause(index))
|
|
463
504
|
}
|
|
464
505
|
|
|
465
506
|
/// Drop the entire global filter set (the `GlobalFilterBar` "Clear" /
|
|
466
|
-
/// element `reset()`)
|
|
467
|
-
/// selection-state cleanup. Master ROLES are untouched — like the
|
|
468
|
-
/// layout, they are workspace structure, not filter state.
|
|
507
|
+
/// element `reset()`).
|
|
469
508
|
pub fn clear_global_filters(&self) -> Vec<PanelId> {
|
|
470
509
|
self.mutate_filters(GlobalFilterSet::clear)
|
|
471
510
|
}
|
|
472
511
|
|
|
473
|
-
/// The master (filter-source) panel ids
|
|
474
|
-
/// this, and a `HashSet`'s per-instance iteration order would make
|
|
475
|
-
/// consecutive `save()` outputs byte-unstable (cf. the `panels`
|
|
476
|
-
/// `BTreeMap`).
|
|
512
|
+
/// The master (filter-source) panel ids.
|
|
477
513
|
pub fn masters(&self) -> Vec<PanelId> {
|
|
478
|
-
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
|
+
|
|
479
523
|
masters.sort();
|
|
480
524
|
masters
|
|
481
525
|
}
|
|
482
526
|
|
|
483
|
-
/// Replace the master role set — the
|
|
484
|
-
/// (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).
|
|
485
530
|
pub fn set_masters(&self, ids: Vec<PanelId>) {
|
|
486
|
-
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
|
+
}
|
|
487
535
|
}
|
|
488
536
|
|
|
489
537
|
/// Whether `id` is a master (filter-source) panel.
|
|
490
538
|
pub fn is_master(&self, id: &PanelId) -> bool {
|
|
491
|
-
self.0
|
|
539
|
+
self.0
|
|
540
|
+
.borrow()
|
|
541
|
+
.panels
|
|
542
|
+
.iter()
|
|
543
|
+
.any(|p| &p.panel.id == id && p.master)
|
|
492
544
|
}
|
|
493
545
|
|
|
494
546
|
/// Toggle `id`'s master/detail role, returning the new state (`true` =
|
|
495
|
-
/// master).
|
|
547
|
+
/// master). `false` (no role) if `id` names no panel.
|
|
496
548
|
pub fn toggle_master(&self, id: &PanelId) -> bool {
|
|
497
549
|
let mut data = self.0.borrow_mut();
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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,
|
|
503
556
|
}
|
|
504
557
|
}
|
|
505
558
|
|
|
@@ -521,7 +574,10 @@ impl Workspace {
|
|
|
521
574
|
pub fn active_panel(&self) -> Option<Panel> {
|
|
522
575
|
let data = self.0.borrow();
|
|
523
576
|
let active = data.active.as_ref()?;
|
|
524
|
-
data.panels
|
|
577
|
+
data.panels
|
|
578
|
+
.iter()
|
|
579
|
+
.find(|p| &p.panel.id == active)
|
|
580
|
+
.map(|p| p.panel.clone())
|
|
525
581
|
}
|
|
526
582
|
|
|
527
583
|
/// The active panel's [`Session`], or `None` with zero panels.
|
|
@@ -536,7 +592,12 @@ impl Workspace {
|
|
|
536
592
|
|
|
537
593
|
/// Look up a [`Panel`] by id.
|
|
538
594
|
pub fn panel(&self, id: &PanelId) -> Option<Panel> {
|
|
539
|
-
self.0
|
|
595
|
+
self.0
|
|
596
|
+
.borrow()
|
|
597
|
+
.panels
|
|
598
|
+
.iter()
|
|
599
|
+
.find(|p| &p.panel.id == id)
|
|
600
|
+
.map(|p| p.panel.clone())
|
|
540
601
|
}
|
|
541
602
|
|
|
542
603
|
/// Resolve a panel by id, or the active panel when `id` is `None` — the
|
|
@@ -552,17 +613,23 @@ impl Workspace {
|
|
|
552
613
|
/// fan-out source (fan-outs collect per-panel results; they never
|
|
553
614
|
/// sequential-abort on one panel's error).
|
|
554
615
|
pub fn panels(&self) -> Vec<Panel> {
|
|
555
|
-
self.0
|
|
616
|
+
self.0
|
|
617
|
+
.borrow()
|
|
618
|
+
.panels
|
|
619
|
+
.iter()
|
|
620
|
+
.map(|p| p.panel.clone())
|
|
621
|
+
.collect()
|
|
556
622
|
}
|
|
557
623
|
|
|
558
624
|
/// The number of PLACED panels (panels minus staged) — the single
|
|
559
625
|
/// source for panel-count chrome (`single`/`multi`, closable,
|
|
560
626
|
/// draggable, `only-child`).
|
|
561
627
|
pub fn placed_count(&self) -> usize {
|
|
562
|
-
|
|
563
|
-
|
|
628
|
+
self.0
|
|
629
|
+
.borrow()
|
|
630
|
+
.panels
|
|
564
631
|
.iter()
|
|
565
|
-
.filter(|p|
|
|
632
|
+
.filter(|p| p.phase == PanelPhase::Placed)
|
|
566
633
|
.count()
|
|
567
634
|
}
|
|
568
635
|
|
|
@@ -572,7 +639,7 @@ impl Workspace {
|
|
|
572
639
|
.borrow()
|
|
573
640
|
.panels
|
|
574
641
|
.iter()
|
|
575
|
-
.map(|p| p.id.clone())
|
|
642
|
+
.map(|p| p.panel.id.clone())
|
|
576
643
|
.collect()
|
|
577
644
|
}
|
|
578
645
|
|
|
@@ -586,56 +653,119 @@ impl Workspace {
|
|
|
586
653
|
self.0.borrow().panels.is_empty()
|
|
587
654
|
}
|
|
588
655
|
|
|
589
|
-
/// Append a [`Panel`]
|
|
590
|
-
/// becomes the active one (there is no other candidate).
|
|
591
|
-
|
|
592
|
-
|
|
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 = {
|
|
593
662
|
let mut data = self.0.borrow_mut();
|
|
594
|
-
if data.active.is_none() {
|
|
663
|
+
let activated = if data.active.is_none() {
|
|
595
664
|
data.active = Some(panel.id.clone());
|
|
596
|
-
|
|
597
|
-
}
|
|
665
|
+
true
|
|
666
|
+
} else {
|
|
667
|
+
false
|
|
668
|
+
};
|
|
598
669
|
|
|
599
670
|
panel
|
|
600
671
|
.renderer
|
|
601
672
|
.set_active_flag(data.active.as_ref() == Some(&panel.id));
|
|
602
|
-
data.panels.push(
|
|
603
|
-
|
|
673
|
+
data.panels.push(PanelEntry {
|
|
674
|
+
panel,
|
|
675
|
+
phase,
|
|
676
|
+
master: false,
|
|
677
|
+
});
|
|
678
|
+
|
|
604
679
|
Self::sync_solo_flags(&data);
|
|
605
|
-
|
|
680
|
+
data.emitter.mark(true, activated, self);
|
|
681
|
+
(phase == PanelPhase::Staging).then(|| data.staged_changed.clone())
|
|
682
|
+
};
|
|
606
683
|
|
|
607
|
-
|
|
684
|
+
if let Some(pubsub) = staged_pubsub {
|
|
685
|
+
pubsub.emit(());
|
|
686
|
+
}
|
|
608
687
|
}
|
|
609
688
|
|
|
610
|
-
/// Hold `panel` in the reservation slot (
|
|
689
|
+
/// Hold `panel` in the reservation slot ([`Reservation::Pending`]):
|
|
611
690
|
/// NOT placed, invisible to every placed-panel consumer, awaiting a
|
|
612
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.
|
|
613
694
|
pub fn reserve_panel(&self, panel: Panel) {
|
|
614
|
-
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);
|
|
615
702
|
}
|
|
616
703
|
|
|
617
704
|
/// The reservation slot's current occupant (shared handles), without a
|
|
618
705
|
/// transfer — a second `load()` on a still-empty element adopts the same
|
|
619
706
|
/// reservation rather than creating a competing one.
|
|
620
707
|
pub fn reserved_panel(&self) -> Option<Panel> {
|
|
621
|
-
self.0.borrow().
|
|
708
|
+
match &self.0.borrow().reservation {
|
|
709
|
+
Reservation::Pending(panel) => Some(panel.clone()),
|
|
710
|
+
_ => None,
|
|
711
|
+
}
|
|
622
712
|
}
|
|
623
713
|
|
|
624
|
-
///
|
|
714
|
+
/// CLAIM the reserved panel: transfer it out of the reservation slot into
|
|
625
715
|
/// the placed set ([`Self::insert_panel`] — auto-activating on an empty
|
|
626
|
-
/// element)
|
|
627
|
-
///
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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);
|
|
631
736
|
Some(panel)
|
|
632
737
|
}
|
|
633
738
|
|
|
634
739
|
/// DISCARD the reserved panel: transfer it out of the reservation slot
|
|
635
740
|
/// WITHOUT placing it, for disposal (an inert `Client` payload with no
|
|
636
|
-
/// 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`].
|
|
637
743
|
pub fn take_reserved(&self) -> Option<Panel> {
|
|
638
|
-
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
|
+
}
|
|
639
769
|
}
|
|
640
770
|
|
|
641
771
|
/// Sync every panel renderer's solo (lone-panel) flag with the current
|
|
@@ -644,83 +774,79 @@ impl Workspace {
|
|
|
644
774
|
/// locked plugin dispatch — see `Renderer::stamp_active`).
|
|
645
775
|
fn sync_solo_flags(data: &WorkspaceData) {
|
|
646
776
|
let is_solo = data.panels.len() == 1;
|
|
647
|
-
for
|
|
648
|
-
panel.renderer.set_solo_flag(is_solo);
|
|
777
|
+
for entry in data.panels.iter() {
|
|
778
|
+
entry.panel.renderer.set_solo_flag(is_solo);
|
|
649
779
|
}
|
|
650
780
|
}
|
|
651
781
|
|
|
652
|
-
/// Remove a [`Panel`] by id, returning it if present.
|
|
653
|
-
///
|
|
654
|
-
///
|
|
655
|
-
///
|
|
656
|
-
///
|
|
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).
|
|
657
787
|
pub fn remove_panel(&self, id: &PanelId) -> Option<Panel> {
|
|
658
|
-
let (removed, changed,
|
|
788
|
+
let (removed, changed, filters_pubsub, was_staged, staged_pubsub) = {
|
|
659
789
|
let mut data = self.0.borrow_mut();
|
|
660
|
-
data.masters.remove(id);
|
|
661
|
-
let staged_removed = data.staged.remove(id);
|
|
662
790
|
let changed = data.filters.set_contribution(id, Vec::new());
|
|
663
|
-
let removed = data
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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 {
|
|
670
801
|
data.active = None;
|
|
671
|
-
data.active_dirty = true;
|
|
672
802
|
}
|
|
673
803
|
|
|
674
|
-
data.layout_dirty |= removed.is_some();
|
|
675
804
|
Self::sync_solo_flags(&data);
|
|
805
|
+
data.emitter.mark(removed.is_some(), deactivated, self);
|
|
676
806
|
(
|
|
677
807
|
removed,
|
|
678
808
|
changed,
|
|
679
809
|
data.filters_changed.clone(),
|
|
680
|
-
|
|
810
|
+
was_staged,
|
|
681
811
|
data.staged_changed.clone(),
|
|
682
812
|
)
|
|
683
813
|
};
|
|
684
814
|
|
|
685
815
|
if changed {
|
|
686
|
-
|
|
816
|
+
filters_pubsub.emit(());
|
|
687
817
|
}
|
|
688
818
|
|
|
689
|
-
if
|
|
819
|
+
if was_staged {
|
|
690
820
|
staged_pubsub.emit(());
|
|
691
821
|
}
|
|
692
822
|
|
|
693
|
-
self.schedule_layout_flush();
|
|
694
823
|
removed
|
|
695
824
|
}
|
|
696
825
|
|
|
697
826
|
/// Set the active panel. Returns `false` (no-op) if `id` is not a known
|
|
698
827
|
/// panel.
|
|
699
828
|
pub fn set_active(&self, id: PanelId) -> bool {
|
|
700
|
-
let
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
panel
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
true
|
|
712
|
-
} else {
|
|
713
|
-
false
|
|
829
|
+
let mut data = self.0.borrow_mut();
|
|
830
|
+
if data.panels.iter().any(|p| p.panel.id == id) {
|
|
831
|
+
let changed = data.active.as_ref() != Some(&id);
|
|
832
|
+
data.active = Some(id);
|
|
833
|
+
for entry in data.panels.iter() {
|
|
834
|
+
entry
|
|
835
|
+
.panel
|
|
836
|
+
.renderer
|
|
837
|
+
.set_active_flag(data.active.as_ref() == Some(&entry.panel.id));
|
|
714
838
|
}
|
|
715
|
-
};
|
|
716
839
|
|
|
717
|
-
|
|
718
|
-
|
|
840
|
+
data.emitter.mark(false, changed, self);
|
|
841
|
+
true
|
|
842
|
+
} else {
|
|
843
|
+
false
|
|
844
|
+
}
|
|
719
845
|
}
|
|
720
846
|
|
|
721
847
|
/// The default [`Client`], if one has been loaded.
|
|
722
848
|
pub fn default_client(&self) -> Option<Client> {
|
|
723
|
-
self.0.borrow().
|
|
849
|
+
self.0.borrow().clients.default.clone()
|
|
724
850
|
}
|
|
725
851
|
|
|
726
852
|
/// The active panel's bound [`Client`], if any — the default target of a
|
|
@@ -736,8 +862,13 @@ impl Workspace {
|
|
|
736
862
|
.borrow()
|
|
737
863
|
.panels
|
|
738
864
|
.iter()
|
|
739
|
-
.filter(|p|
|
|
740
|
-
|
|
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())
|
|
741
872
|
.collect()
|
|
742
873
|
}
|
|
743
874
|
|
|
@@ -747,26 +878,27 @@ impl Workspace {
|
|
|
747
878
|
/// [`Workspace::panels_for_client`]) — `clients()` unions in live panel
|
|
748
879
|
/// sessions, so a lingering panel would resurrect it.
|
|
749
880
|
pub fn remove_client(&self, name: &str) {
|
|
750
|
-
|
|
751
|
-
data.clients.retain(|c| c.get_name() != name);
|
|
752
|
-
if data
|
|
753
|
-
.default_client
|
|
754
|
-
.as_ref()
|
|
755
|
-
.is_some_and(|c| c.get_name() == name)
|
|
756
|
-
{
|
|
757
|
-
data.default_client = None;
|
|
758
|
-
}
|
|
881
|
+
self.0.borrow_mut().clients.remove(name);
|
|
759
882
|
}
|
|
760
883
|
|
|
761
884
|
/// Record the default [`Client`] if not already set (first-wins, matching
|
|
762
885
|
/// the "first `Client` passed to `load()` is the default" rule). Always
|
|
763
886
|
/// registers the client (see [`Workspace::register_client`]) — first-wins
|
|
764
|
-
/// 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.
|
|
765
889
|
pub fn set_default_client(&self, client: Client) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
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);
|
|
770
902
|
}
|
|
771
903
|
}
|
|
772
904
|
|
|
@@ -774,27 +906,15 @@ impl Workspace {
|
|
|
774
906
|
/// same (globally unique) name isn't already present. Emits
|
|
775
907
|
/// `client_registered` — outside the borrow — for a genuinely-new client.
|
|
776
908
|
pub fn register_client(&self, client: Client) {
|
|
777
|
-
let pubsub =
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
.iter()
|
|
782
|
-
.any(|c| c.get_name() == client.get_name())
|
|
783
|
-
{
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
data.clients.push(client.clone());
|
|
788
|
-
data.client_registered.clone()
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
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
|
+
}
|
|
792
913
|
}
|
|
793
914
|
|
|
794
|
-
/// A handle to the `client_registered` PubSub (see
|
|
795
|
-
/// [`WorkspaceData::client_registered`]).
|
|
915
|
+
/// A handle to the `client_registered` PubSub (see [`ClientRegistry`]).
|
|
796
916
|
pub fn client_registered(&self) -> Rc<PubSub<Client>> {
|
|
797
|
-
self.0.borrow().client_registered.clone()
|
|
917
|
+
self.0.borrow().clients.client_registered.clone()
|
|
798
918
|
}
|
|
799
919
|
|
|
800
920
|
/// All loaded [`Client`]s: the registry, unioned with every panel
|
|
@@ -802,9 +922,9 @@ impl Workspace {
|
|
|
802
922
|
/// bypasses registration), deduped by name in registration order.
|
|
803
923
|
pub fn clients(&self) -> Vec<Client> {
|
|
804
924
|
let data = self.0.borrow();
|
|
805
|
-
let mut clients = data.clients.clone();
|
|
806
|
-
for
|
|
807
|
-
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()
|
|
808
928
|
&& !clients.iter().any(|c| c.get_name() == client.get_name())
|
|
809
929
|
{
|
|
810
930
|
clients.push(client);
|
|
@@ -935,4 +1055,47 @@ mod tests {
|
|
|
935
1055
|
assert_eq!(s.flatten(), Vec::<Filter>::new());
|
|
936
1056
|
assert_eq!(s.clear(), (false, Vec::new()));
|
|
937
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
|
+
}
|
|
938
1101
|
}
|