@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/renderer.rs
CHANGED
|
@@ -142,19 +142,14 @@ pub struct RendererData {
|
|
|
142
142
|
/// coexist there.
|
|
143
143
|
slot_name: RefCell<Option<String>>,
|
|
144
144
|
|
|
145
|
-
/// This panel's
|
|
146
|
-
///
|
|
145
|
+
/// This panel's theme name — CONCRETE, resolved once at creation from
|
|
146
|
+
/// the config's `theme`, else the host's, else the registry default.
|
|
147
|
+
/// There is no "unthemed panel" that resolves against live element
|
|
148
|
+
/// state at draw time: a registry re-ordering must never repaint a
|
|
149
|
+
/// panel, so nothing but an explicit write may change this. `None` only
|
|
150
|
+
/// while no themes exist at all.
|
|
147
151
|
theme: RefCell<Option<String>>,
|
|
148
152
|
|
|
149
|
-
/// The registry default theme name (first registered), cached here so
|
|
150
|
-
/// LOCKED draw paths can resolve this panel's EFFECTIVE theme
|
|
151
|
-
/// synchronously (`Presentation::get_default_theme_name` is async — it
|
|
152
|
-
/// awaits theme discovery). Seeded by every content-load path before its
|
|
153
|
-
/// first locked draw (`restore_and_render`, `load()`, the
|
|
154
|
-
/// resize-observer's deferred first render) and kept fresh by the root's
|
|
155
|
-
/// `UpdatePresentation` default-theme fan-out and `resetThemes`.
|
|
156
|
-
default_theme: RefCell<Option<String>>,
|
|
157
|
-
|
|
158
153
|
/// Whether the active plugin has completed a draw. An EXPLICIT flag —
|
|
159
154
|
/// not inferred from DOM connectedness — because plugin elements may be
|
|
160
155
|
/// mounted eagerly (at panel creation / draw start, before the view
|
|
@@ -276,7 +271,6 @@ impl Renderer {
|
|
|
276
271
|
on_render_limits_changed: Default::default(),
|
|
277
272
|
slot_name: Default::default(),
|
|
278
273
|
theme: Default::default(),
|
|
279
|
-
default_theme: Default::default(),
|
|
280
274
|
has_drawn: Cell::new(false),
|
|
281
275
|
captured_theme: Default::default(),
|
|
282
276
|
cached_context: Default::default(),
|
|
@@ -299,45 +293,24 @@ impl Renderer {
|
|
|
299
293
|
self.0.slot_name.borrow().clone()
|
|
300
294
|
}
|
|
301
295
|
|
|
302
|
-
/// Set this panel's theme name
|
|
296
|
+
/// Set this panel's theme name. Callers must pass a CONCRETE name —
|
|
297
|
+
/// resolve "the default" through `Presentation` before calling, never by
|
|
298
|
+
/// leaving this empty.
|
|
303
299
|
pub fn set_theme(&self, name: Option<String>) {
|
|
304
300
|
*self.0.theme.borrow_mut() = name;
|
|
305
301
|
}
|
|
306
302
|
|
|
307
|
-
/// This panel's
|
|
303
|
+
/// This panel's theme name.
|
|
308
304
|
pub fn theme(&self) -> Option<String> {
|
|
309
305
|
self.0.theme.borrow().clone()
|
|
310
306
|
}
|
|
311
307
|
|
|
312
|
-
/// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`]
|
|
313
|
-
///
|
|
314
|
-
///
|
|
315
|
-
/// attribute-removal). The shared "stamp-with-commit" head of every
|
|
316
|
-
/// per-panel theme mutation site.
|
|
308
|
+
/// [`Self::set_theme`] plus a synchronous [`Self::stamp_theme`] — the
|
|
309
|
+
/// shared "stamp-with-commit" head of every per-panel theme mutation
|
|
310
|
+
/// site.
|
|
317
311
|
pub fn set_theme_stamped(&self, theme: Option<String>) {
|
|
318
|
-
let stamp = theme.is_some() || self.default_theme().is_some();
|
|
319
312
|
self.set_theme(theme);
|
|
320
|
-
|
|
321
|
-
self.stamp_theme(None);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/// Set the cached registry default theme name (see the field docs on
|
|
326
|
-
/// [`RendererData`]).
|
|
327
|
-
pub fn set_default_theme(&self, name: Option<String>) {
|
|
328
|
-
*self.0.default_theme.borrow_mut() = name;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/// The cached registry default theme name.
|
|
332
|
-
pub fn default_theme(&self) -> Option<String> {
|
|
333
|
-
self.0.default_theme.borrow().clone()
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/// This panel's EFFECTIVE theme: its own ([`Self::theme`]), else the
|
|
337
|
-
/// cached registry default ([`Self::default_theme`]) — the value
|
|
338
|
-
/// [`Self::stamp_theme`] stamps.
|
|
339
|
-
pub fn effective_theme(&self) -> Option<String> {
|
|
340
|
-
self.theme().or_else(|| self.default_theme())
|
|
313
|
+
self.stamp_theme(None);
|
|
341
314
|
}
|
|
342
315
|
|
|
343
316
|
/// Whether the active plugin's captured `--psp-*` CSS is STALE — the
|
|
@@ -352,7 +325,7 @@ impl Renderer {
|
|
|
352
325
|
/// paint captures fresh by construction ("stamp before draw").
|
|
353
326
|
pub fn needs_restyle(&self) -> bool {
|
|
354
327
|
match &*self.0.captured_theme.borrow() {
|
|
355
|
-
Some(captured) => *captured != self.
|
|
328
|
+
Some(captured) => *captured != self.theme(),
|
|
356
329
|
None => false,
|
|
357
330
|
}
|
|
358
331
|
}
|
|
@@ -20,11 +20,16 @@ use crate::config::PluginStaticConfig;
|
|
|
20
20
|
|
|
21
21
|
#[extend::ext]
|
|
22
22
|
pub impl ViewConfigUpdate {
|
|
23
|
-
/// Coerce this update's `group_rollup_mode` to one
|
|
24
|
-
/// (`PluginStaticConfig
|
|
25
|
-
/// absent mode counts as `
|
|
26
|
-
///
|
|
27
|
-
/// when the update
|
|
23
|
+
/// Coerce this update's `group_rollup_mode` / `split_rollup_mode` to one
|
|
24
|
+
/// the plugin accepts (`PluginStaticConfig::*_rollup_modes`,
|
|
25
|
+
/// feature-filtered). An absent mode counts as `current`'s committed mode
|
|
26
|
+
/// for the acceptance check — `ViewConfig::apply_update` KEEPS the
|
|
27
|
+
/// committed mode when the update's field is `None`, so acceptance must
|
|
28
|
+
/// be judged against the mode that will actually be in effect. A
|
|
29
|
+
/// hardcoded-default fallback here let a plugin swap silently retain a
|
|
30
|
+
/// mode the new plugin excludes (e.g. Datagrid's `split_rollup_mode:
|
|
31
|
+
/// "rollup"` surviving a `restore({plugin: "Y Bar"})` onto a flat-only
|
|
32
|
+
/// chart).
|
|
28
33
|
///
|
|
29
34
|
/// Split out of [`Self::set_update_column_defaults`] so same-plugin
|
|
30
35
|
/// restores can enforce the mode WITHOUT the column-defaulting below —
|
|
@@ -33,6 +38,7 @@ pub impl ViewConfigUpdate {
|
|
|
33
38
|
fn set_update_rollup_defaults(
|
|
34
39
|
&mut self,
|
|
35
40
|
metadata: &SessionMetadata,
|
|
41
|
+
current: &ViewConfig,
|
|
36
42
|
config_static: &PluginStaticConfig,
|
|
37
43
|
) {
|
|
38
44
|
let rollup_features = metadata
|
|
@@ -44,7 +50,7 @@ pub impl ViewConfigUpdate {
|
|
|
44
50
|
if !group_rollups.contains(
|
|
45
51
|
self.group_rollup_mode
|
|
46
52
|
.as_ref()
|
|
47
|
-
.unwrap_or(&
|
|
53
|
+
.unwrap_or(¤t.group_rollup_mode),
|
|
48
54
|
) {
|
|
49
55
|
self.group_rollup_mode = group_rollups.first().cloned();
|
|
50
56
|
tracing::debug!(
|
|
@@ -62,7 +68,7 @@ pub impl ViewConfigUpdate {
|
|
|
62
68
|
if !split_rollups.contains(
|
|
63
69
|
self.split_rollup_mode
|
|
64
70
|
.as_ref()
|
|
65
|
-
.unwrap_or(&
|
|
71
|
+
.unwrap_or(¤t.split_rollup_mode),
|
|
66
72
|
) {
|
|
67
73
|
self.split_rollup_mode = split_rollups.first().cloned();
|
|
68
74
|
tracing::debug!(
|
|
@@ -81,10 +87,11 @@ pub impl ViewConfigUpdate {
|
|
|
81
87
|
fn set_update_column_defaults(
|
|
82
88
|
&mut self,
|
|
83
89
|
metadata: &SessionMetadata,
|
|
90
|
+
current: &ViewConfig,
|
|
84
91
|
columns: &[Option<String>],
|
|
85
92
|
config_static: &PluginStaticConfig,
|
|
86
93
|
) {
|
|
87
|
-
self.set_update_rollup_defaults(metadata, config_static);
|
|
94
|
+
self.set_update_rollup_defaults(metadata, current, config_static);
|
|
88
95
|
|
|
89
96
|
if let (None, Some(min_cols)) = (&self.columns, config_static.min_config_columns) {
|
|
90
97
|
let names_len = config_static.config_column_names.len();
|
|
@@ -187,7 +187,7 @@ impl SessionMetadata {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
pub fn get_expression_columns(&self) -> impl Iterator<Item = &'_ String> {
|
|
190
|
-
try {
|
|
190
|
+
let mut columns = try {
|
|
191
191
|
self.as_ref()?
|
|
192
192
|
.expr_meta
|
|
193
193
|
.as_ref()?
|
|
@@ -197,6 +197,10 @@ impl SessionMetadata {
|
|
|
197
197
|
}
|
|
198
198
|
.into_iter()
|
|
199
199
|
.flatten()
|
|
200
|
+
.collect::<Vec<_>>();
|
|
201
|
+
|
|
202
|
+
columns.sort_unstable();
|
|
203
|
+
columns.into_iter()
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/// Returns the full original expression `String` for an expression alias.
|
package/src/rust/session.rs
CHANGED
|
@@ -719,9 +719,11 @@ impl Session {
|
|
|
719
719
|
config_static: &PluginStaticConfig,
|
|
720
720
|
) {
|
|
721
721
|
use self::column_defaults_update::*;
|
|
722
|
+
let config = self.get_view_config();
|
|
722
723
|
config_update.set_update_column_defaults(
|
|
723
724
|
&self.metadata(),
|
|
724
|
-
&
|
|
725
|
+
&config,
|
|
726
|
+
&config.columns,
|
|
725
727
|
config_static,
|
|
726
728
|
)
|
|
727
729
|
}
|
|
@@ -737,7 +739,11 @@ impl Session {
|
|
|
737
739
|
config_static: &PluginStaticConfig,
|
|
738
740
|
) {
|
|
739
741
|
use self::column_defaults_update::*;
|
|
740
|
-
config_update.set_update_rollup_defaults(
|
|
742
|
+
config_update.set_update_rollup_defaults(
|
|
743
|
+
&self.metadata(),
|
|
744
|
+
&self.get_view_config(),
|
|
745
|
+
config_static,
|
|
746
|
+
)
|
|
741
747
|
}
|
|
742
748
|
|
|
743
749
|
/// Apply a `ViewConfigUpdate` to the live config — the ONLY view-config
|
|
@@ -889,7 +895,7 @@ impl Session {
|
|
|
889
895
|
.pending_dispatches
|
|
890
896
|
.set(self.0.pending_dispatches.get() + 1);
|
|
891
897
|
let session = self.clone();
|
|
892
|
-
ApiFuture::
|
|
898
|
+
ApiFuture::spawn_named("config-update-dispatch", async move {
|
|
893
899
|
let result = fut.await;
|
|
894
900
|
let remaining = session.0.pending_dispatches.get() - 1;
|
|
895
901
|
session.0.pending_dispatches.set(remaining);
|
|
@@ -1165,9 +1171,6 @@ impl Session {
|
|
|
1165
1171
|
let old = self.borrow_mut().view_sub.take();
|
|
1166
1172
|
ApiFuture::spawn(old.delete());
|
|
1167
1173
|
self.borrow_mut().view_sub = Some(sub);
|
|
1168
|
-
// REBUILD: a `View` was genuinely constructed and bound — the one
|
|
1169
|
-
// place `view_created` may fire, and the one place the `FreshView`
|
|
1170
|
-
// full-draw witness is minted from a rebuild.
|
|
1171
1174
|
self.view_created.emit(());
|
|
1172
1175
|
self.commit_reconciled.emit(());
|
|
1173
1176
|
match self.get_view() {
|
|
@@ -20,7 +20,7 @@ use crate::renderer::*;
|
|
|
20
20
|
use crate::session::{ResetOptions, Session};
|
|
21
21
|
use crate::tasks::*;
|
|
22
22
|
use crate::utils::*;
|
|
23
|
-
use crate::workspace::{Panel, PanelId, Workspace};
|
|
23
|
+
use crate::workspace::{Panel, PanelId, PanelPhase, Workspace};
|
|
24
24
|
use crate::*;
|
|
25
25
|
|
|
26
26
|
/// Build the full set of subscriptions a [`Panel`] owns for its lifetime: its
|
|
@@ -70,29 +70,19 @@ fn wire_panel_render_sub(session: &Session, renderer: &Renderer) -> Subscription
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/// Create a new independent panel (own `Session` + `Renderer` + id) and
|
|
73
|
-
/// restore `
|
|
74
|
-
/// create-if-missing upsert
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
///
|
|
79
|
-
///
|
|
80
|
-
/// A `table` is OPTIONAL here: an update with none creates a deferred
|
|
81
|
-
/// panel that a later `load()` binds, which is the pre-existing
|
|
82
|
-
/// `restore`-then-`load` contract and the same state the reserved
|
|
83
|
-
/// `load()` path produces. The routes where a table-less panel WOULD be
|
|
84
|
-
/// permanently blank require one in their own argument types instead —
|
|
85
|
-
/// `addPanel`'s [`ViewerConfigInitial`], `WorkspaceConfigUpdate`'s
|
|
86
|
-
/// `panels` entries, and the agent's `add_panel` — so the guarantee sits
|
|
87
|
-
/// at the boundary that owns it rather than here, which would sweep
|
|
88
|
-
/// `restore`'s patch in with them.
|
|
73
|
+
/// restore `config` into it. Shared by `addPanel` and `restore`'s
|
|
74
|
+
/// create-if-missing upsert. `id` is the panel's id — provided when
|
|
75
|
+
/// restoring into a specific named slot, or `None` to generate a fresh
|
|
76
|
+
/// one. `theme` is stripped (element-level, not per-panel) and `client` —
|
|
77
|
+
/// or the element's default client when `None` — is bound so the config's
|
|
78
|
+
/// `table` resolves against it.
|
|
89
79
|
pub(crate) async fn create_panel(
|
|
90
80
|
elem: &HtmlElement,
|
|
91
81
|
presentation: &Presentation,
|
|
92
82
|
workspace: &Workspace,
|
|
93
83
|
notify: &Callback<()>,
|
|
94
84
|
id: Option<PanelId>,
|
|
95
|
-
|
|
85
|
+
config: ViewerConfigInitial,
|
|
96
86
|
client: Option<perspective_client::Client>,
|
|
97
87
|
) -> ApiResult<PanelId> {
|
|
98
88
|
let (id, session, renderer, update) = create_panel_model(
|
|
@@ -100,36 +90,25 @@ pub(crate) async fn create_panel(
|
|
|
100
90
|
presentation,
|
|
101
91
|
workspace,
|
|
102
92
|
id,
|
|
103
|
-
|
|
93
|
+
config.into(),
|
|
104
94
|
client,
|
|
105
|
-
Placement::
|
|
95
|
+
Placement::Staged,
|
|
106
96
|
);
|
|
107
97
|
|
|
108
98
|
stamp_global_overlay(workspace, &id, &session);
|
|
109
|
-
// STAGE the panel (model-first): `MainPanel` renders its plugin slot
|
|
110
|
-
// into a hidden pre-sized wrapper — not a layout cell — so the restore
|
|
111
|
-
// below completes its first draw invisibly at (near-)final size. The
|
|
112
|
-
// promote below then re-renders, and `MainPanel::reconcile` inserts an
|
|
113
|
-
// ALREADY-DRAWN panel: the one layout transition reveals content,
|
|
114
|
-
// never a blank frame.
|
|
115
|
-
workspace.stage_panel(&id);
|
|
116
99
|
notify.emit(());
|
|
117
100
|
|
|
118
|
-
|
|
119
|
-
// loading — the pre-staging progressive reveal) rather than leaving
|
|
120
|
-
// the layout without feedback. Races the completion promote below on
|
|
121
|
-
// the staged flag; only the winner re-renders.
|
|
122
|
-
{
|
|
101
|
+
ApiFuture::spawn({
|
|
123
102
|
clone!(workspace, id, notify);
|
|
124
|
-
|
|
103
|
+
async move {
|
|
125
104
|
set_timeout(STAGING_DEADLINE_MS).await?;
|
|
126
|
-
if workspace.
|
|
105
|
+
if workspace.promote(&id) {
|
|
127
106
|
notify.emit(());
|
|
128
107
|
}
|
|
129
108
|
|
|
130
109
|
Ok(())
|
|
131
|
-
}
|
|
132
|
-
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
133
112
|
|
|
134
113
|
// A fresh panel is never the active one, so it needs no `root` for the
|
|
135
114
|
// (active-only) settings-sidebar sequencing.
|
|
@@ -146,7 +125,7 @@ pub(crate) async fn create_panel(
|
|
|
146
125
|
|
|
147
126
|
// Promote on completion AND error — a failed restore's error state
|
|
148
127
|
// must become visible too.
|
|
149
|
-
if workspace.
|
|
128
|
+
if workspace.promote(&id) {
|
|
150
129
|
notify.emit(());
|
|
151
130
|
}
|
|
152
131
|
|
|
@@ -156,10 +135,17 @@ pub(crate) async fn create_panel(
|
|
|
156
135
|
|
|
157
136
|
/// Where [`create_panel_model`] registers the new panel model.
|
|
158
137
|
pub(crate) enum Placement {
|
|
159
|
-
/// Into the placed panel set
|
|
160
|
-
///
|
|
138
|
+
/// Into the placed panel set at [`PanelPhase::Placed`] — a live, visible
|
|
139
|
+
/// panel from the start (`restoreWorkspace` mounts panels directly at
|
|
140
|
+
/// their saved layout positions).
|
|
161
141
|
Placed,
|
|
162
142
|
|
|
143
|
+
/// Into the placed panel set at [`PanelPhase::Staging`] — withheld from
|
|
144
|
+
/// the layout while its first draw completes in the hidden staging
|
|
145
|
+
/// wrapper, then promoted ([`Workspace::promote`]) by restore completion
|
|
146
|
+
/// or the [`STAGING_DEADLINE_MS`] deadline, whichever wins.
|
|
147
|
+
Staged,
|
|
148
|
+
|
|
163
149
|
/// Into the reservation slot ([`Workspace::reserve_panel`]) — a pending
|
|
164
150
|
/// `load()`'s first panel, held out of the placed set until it is
|
|
165
151
|
/// claimed ([`place_reserved`]) or discarded.
|
|
@@ -180,32 +166,23 @@ pub(crate) fn create_panel_model(
|
|
|
180
166
|
client: Option<perspective_client::Client>,
|
|
181
167
|
placement: Placement,
|
|
182
168
|
) -> (PanelId, Session, Renderer, ViewerConfigUpdate) {
|
|
183
|
-
// Authored-theme boot: the FIRST panel of an empty element with no explicit
|
|
184
|
-
// theme adopts the element's `theme` attribute (a one-time initial
|
|
185
|
-
// selection; the attribute is otherwise an active-panel mirror). Captured
|
|
186
|
-
// before `insert_panel` makes the workspace non-empty.
|
|
187
|
-
let boot_theme = (workspace.is_empty() && matches!(update.theme, OptionalUpdate::Missing))
|
|
188
|
-
.then(|| elem.get_attribute("theme"))
|
|
189
|
-
.flatten();
|
|
190
|
-
|
|
191
169
|
let session = Session::new();
|
|
192
170
|
let renderer = Renderer::new(elem);
|
|
193
171
|
let id = id.unwrap_or_else(|| workspace.generate_id());
|
|
194
172
|
renderer.set_slot_name(id.as_str());
|
|
195
|
-
renderer.set_default_theme(presentation.default_theme_name_sync());
|
|
196
173
|
let subs = wire_panel_subs(elem, presentation, &session, &renderer);
|
|
197
174
|
let panel = Panel::new(id.clone(), session.clone(), renderer.clone(), subs);
|
|
198
175
|
match placement {
|
|
199
|
-
Placement::Placed => workspace.insert_panel(panel),
|
|
176
|
+
Placement::Placed => workspace.insert_panel(panel, PanelPhase::Placed),
|
|
177
|
+
Placement::Staged => workspace.insert_panel(panel, PanelPhase::Staging),
|
|
200
178
|
Placement::Reserved => workspace.reserve_panel(panel),
|
|
201
179
|
}
|
|
202
180
|
|
|
203
181
|
update.settings = OptionalUpdate::Missing;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
182
|
+
renderer.set_theme(match &update.theme {
|
|
183
|
+
OptionalUpdate::Update(theme) => Some(theme.clone()),
|
|
184
|
+
_ => presentation.active_theme_name_sync(),
|
|
185
|
+
});
|
|
209
186
|
|
|
210
187
|
update.theme = OptionalUpdate::Missing;
|
|
211
188
|
if let Some(client) = client.or_else(|| workspace.default_client()) {
|
|
@@ -221,8 +198,18 @@ pub(crate) fn create_panel_model(
|
|
|
221
198
|
(id, session, renderer, update)
|
|
222
199
|
}
|
|
223
200
|
|
|
224
|
-
|
|
225
|
-
|
|
201
|
+
/// Claim the pending `load()` reservation into the placed set
|
|
202
|
+
/// ([`Workspace::claim_reserved`]), stamping the global-filter overlay and
|
|
203
|
+
/// notifying the root. `has_table` is whether the claimant carried a
|
|
204
|
+
/// `table`, recorded atomically with the claim — pass `true` when `load()`
|
|
205
|
+
/// itself places its reservation (a `Table` payload, or error surfacing),
|
|
206
|
+
/// which is not a restore claim and must never arm the epilogue's eviction.
|
|
207
|
+
pub(crate) fn place_reserved(
|
|
208
|
+
workspace: &Workspace,
|
|
209
|
+
notify: &Callback<()>,
|
|
210
|
+
has_table: bool,
|
|
211
|
+
) -> Option<Panel> {
|
|
212
|
+
let panel = workspace.claim_reserved(has_table)?;
|
|
226
213
|
stamp_global_overlay(workspace, &panel.id, &panel.session);
|
|
227
214
|
notify.emit(());
|
|
228
215
|
Some(panel)
|
|
@@ -230,7 +217,7 @@ pub(crate) fn place_reserved(workspace: &Workspace, notify: &Callback<()>) -> Op
|
|
|
230
217
|
|
|
231
218
|
/// Tear down a [`Panel`] already removed from the [`Workspace`]: dispose its
|
|
232
219
|
/// renderer (slot-scoped plugin + light-DOM cleanup) and eject its table.
|
|
233
|
-
/// Shared by the root's `ClosePanel` handler and
|
|
220
|
+
/// Shared by the root's `ClosePanel` handler and `restoreWorkspace`'s
|
|
234
221
|
/// batch replacement of the pre-existing panel set.
|
|
235
222
|
pub(crate) fn eject_panel(panel: Panel) -> ApiFuture<()> {
|
|
236
223
|
let was_errored = panel.session.is_errored();
|
|
@@ -18,14 +18,22 @@ use perspective_client::clone;
|
|
|
18
18
|
use perspective_client::config::{Expression, ViewConfigUpdate};
|
|
19
19
|
|
|
20
20
|
use super::apply_and_render;
|
|
21
|
-
use crate::presentation::{
|
|
21
|
+
use crate::presentation::{
|
|
22
|
+
ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings, Presentation,
|
|
23
|
+
};
|
|
22
24
|
use crate::renderer::Renderer;
|
|
23
25
|
use crate::session::Session;
|
|
24
26
|
use crate::*;
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
fn reopen(presentation: &Presentation, name: String) {
|
|
29
|
+
presentation.set_open_column_settings(Some(OpenColumnSettings {
|
|
30
|
+
target: Some(ColumnSettingsTarget::Column(name)),
|
|
31
|
+
tab: Some(ColumnSettingsTab::Attributes),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Replace the expression at `old_name` with `new_expr` and re-render,
|
|
36
|
+
/// moving the column-settings drawer onto the renamed expression.
|
|
29
37
|
pub fn update_expr(
|
|
30
38
|
session: &Session,
|
|
31
39
|
renderer: &Renderer,
|
|
@@ -39,25 +47,21 @@ pub fn update_expr(
|
|
|
39
47
|
.to_props()
|
|
40
48
|
.create_replace_expression_update(&old_name, &new_expr);
|
|
41
49
|
|
|
42
|
-
apply_and_render(&session, &renderer, update)
|
|
43
|
-
presentation.
|
|
44
|
-
|
|
45
|
-
tab: Some(ColumnSettingsTab::Attributes),
|
|
46
|
-
}));
|
|
47
|
-
|
|
50
|
+
let run = apply_and_render(&session, &renderer, update)?;
|
|
51
|
+
reopen(&presentation, new_expr.name.to_string());
|
|
52
|
+
run.await?;
|
|
48
53
|
Ok(())
|
|
49
54
|
});
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
/// Insert `expr` into the session's expressions and re-render
|
|
53
|
-
///
|
|
57
|
+
/// Insert `expr` into the session's expressions and re-render, moving the
|
|
58
|
+
/// column-settings drawer onto the new expression.
|
|
54
59
|
pub fn save_expr(
|
|
55
60
|
session: &Session,
|
|
56
61
|
renderer: &Renderer,
|
|
57
62
|
presentation: &Presentation,
|
|
58
63
|
expr: Expression,
|
|
59
64
|
) -> ApiResult<()> {
|
|
60
|
-
let presentation = presentation.clone();
|
|
61
65
|
let expr_name: String = expr.name.clone().into();
|
|
62
66
|
let task = {
|
|
63
67
|
let mut serde_exprs = session.get_view_config().expressions.clone();
|
|
@@ -68,16 +72,8 @@ pub fn save_expr(
|
|
|
68
72
|
})
|
|
69
73
|
}?;
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
presentation.set_open_column_settings(Some(OpenColumnSettings {
|
|
74
|
-
locator: Some(ColumnLocator::Expression(expr_name)),
|
|
75
|
-
tab: Some(ColumnSettingsTab::Attributes),
|
|
76
|
-
}));
|
|
77
|
-
|
|
78
|
-
Ok(())
|
|
79
|
-
});
|
|
80
|
-
|
|
75
|
+
reopen(presentation, expr_name);
|
|
76
|
+
ApiFuture::spawn(task);
|
|
81
77
|
Ok(())
|
|
82
78
|
}
|
|
83
79
|
|
|
@@ -14,14 +14,16 @@ use perspective_client::clone;
|
|
|
14
14
|
use perspective_client::config::{Filter, ViewConfigUpdate, WindowSpec};
|
|
15
15
|
|
|
16
16
|
use super::apply_and_render;
|
|
17
|
-
use crate::presentation::{
|
|
17
|
+
use crate::presentation::{
|
|
18
|
+
ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings, Presentation,
|
|
19
|
+
};
|
|
18
20
|
use crate::renderer::Renderer;
|
|
19
21
|
use crate::session::Session;
|
|
20
22
|
use crate::*;
|
|
21
23
|
|
|
22
24
|
fn reopen(presentation: &Presentation, name: String) {
|
|
23
25
|
presentation.set_open_column_settings(Some(OpenColumnSettings {
|
|
24
|
-
|
|
26
|
+
target: Some(ColumnSettingsTarget::Column(name)),
|
|
25
27
|
tab: Some(ColumnSettingsTab::Window),
|
|
26
28
|
}));
|
|
27
29
|
}
|
|
@@ -36,7 +38,6 @@ pub fn save_window(
|
|
|
36
38
|
name: String,
|
|
37
39
|
spec: WindowSpec,
|
|
38
40
|
) -> ApiResult<()> {
|
|
39
|
-
let presentation = presentation.clone();
|
|
40
41
|
let task = {
|
|
41
42
|
let mut windows = session.get_view_config().windows.clone();
|
|
42
43
|
windows.insert(name.clone(), spec);
|
|
@@ -46,12 +47,8 @@ pub fn save_window(
|
|
|
46
47
|
})
|
|
47
48
|
}?;
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
reopen(&presentation, name);
|
|
52
|
-
Ok(())
|
|
53
|
-
});
|
|
54
|
-
|
|
50
|
+
reopen(presentation, name);
|
|
51
|
+
ApiFuture::spawn(task);
|
|
55
52
|
Ok(())
|
|
56
53
|
}
|
|
57
54
|
|
|
@@ -134,8 +131,9 @@ pub fn update_window(
|
|
|
134
131
|
update
|
|
135
132
|
};
|
|
136
133
|
|
|
137
|
-
apply_and_render(&session, &renderer, update)
|
|
134
|
+
let run = apply_and_render(&session, &renderer, update)?;
|
|
138
135
|
reopen(&presentation, new_name);
|
|
136
|
+
run.await?;
|
|
139
137
|
Ok(())
|
|
140
138
|
});
|
|
141
139
|
}
|
package/src/rust/tasks/eject.rs
CHANGED
|
@@ -23,11 +23,14 @@ use crate::workspace::Workspace;
|
|
|
23
23
|
/// Tear down the entire viewer: dispose EVERY panel's engines (emit
|
|
24
24
|
/// `table_unloaded`, destroy the renderer under its own draw lock, eject the
|
|
25
25
|
/// session), then drop the Yew root once. Fans out over all panels — not just
|
|
26
|
-
/// the seed — so panels added via `addPanel` or
|
|
26
|
+
/// the seed — so panels added via `addPanel` or `restoreWorkspace` don't
|
|
27
27
|
/// leak their `View`/`Table` + plugin when the element is deleted or ejected.
|
|
28
28
|
pub fn delete_all<T: Component>(workspace: &Workspace, root: &Root<T>) -> ApiFuture<()> {
|
|
29
29
|
clone!(workspace, root);
|
|
30
30
|
ApiFuture::new(async move {
|
|
31
|
+
// A staged-but-unapplied `restoreWorkspace` layout tree must not
|
|
32
|
+
// outlive the panels it names.
|
|
33
|
+
workspace.take_pending_layout();
|
|
31
34
|
let panels = workspace
|
|
32
35
|
.take_reserved()
|
|
33
36
|
.into_iter()
|
|
@@ -285,12 +285,12 @@ pub(crate) async fn locked_run(
|
|
|
285
285
|
|
|
286
286
|
let view_config_snapshot = session.get_view_config().clone();
|
|
287
287
|
let plugin_config_changed =
|
|
288
|
-
renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config)
|
|
288
|
+
renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config)?;
|
|
289
289
|
let columns_config_changed = renderer.update_columns_configs(
|
|
290
290
|
&view_config_snapshot,
|
|
291
291
|
&session,
|
|
292
292
|
spec.columns_config,
|
|
293
|
-
)
|
|
293
|
+
)?;
|
|
294
294
|
|
|
295
295
|
let changed = plugin_config_changed || columns_config_changed;
|
|
296
296
|
if changed || plugin_swapped {
|
|
@@ -131,11 +131,14 @@ pub async fn presize_visible_panels_grown(
|
|
|
131
131
|
presize_visible_panels_scaled(workspace, elem, width_ratio, height_ratio).await
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/// Pre-size each visible plugin to its SHRUNK
|
|
135
|
-
///
|
|
136
|
-
/// the
|
|
137
|
-
///
|
|
138
|
-
///
|
|
134
|
+
/// Pre-size each visible plugin to its cell SHRUNK by `(delta_w, delta_h)` —
|
|
135
|
+
/// GROWN when a delta is negative — *before* the layout change commits. Used
|
|
136
|
+
/// by the settings-pane open (P2, deltas `(layout_area.w − mpc.w,
|
|
137
|
+
/// main_column.h − mpc.h)` cached at the last close — the pane and divider
|
|
138
|
+
/// width and the docked status-bar height, both stable across a close/open
|
|
139
|
+
/// cycle since the pane width persists in the override) and by the
|
|
140
|
+
/// column-settings pin toggle and docked-drawer mount/unmount (signed delta
|
|
141
|
+
/// measured live at toggle time).
|
|
139
142
|
pub async fn presize_visible_panels_open(
|
|
140
143
|
workspace: &Workspace,
|
|
141
144
|
elem: &web_sys::HtmlElement,
|
|
@@ -199,6 +202,7 @@ async fn presize_visible_panels_scaled(
|
|
|
199
202
|
return StagedPresents::default();
|
|
200
203
|
}
|
|
201
204
|
|
|
205
|
+
let mpc = shadow_rect(elem, "#main_panel_container");
|
|
202
206
|
let mut last_chrome: Option<(f64, f64)> = None;
|
|
203
207
|
let targets = workspace
|
|
204
208
|
.panels()
|
|
@@ -222,6 +226,23 @@ async fn presize_visible_panels_scaled(
|
|
|
222
226
|
|
|
223
227
|
last_chrome = chrome.or(last_chrome);
|
|
224
228
|
let (cw, ch) = last_chrome.unwrap_or(super::CHROME_FALLBACK);
|
|
229
|
+
|
|
230
|
+
// Per axis, chrome can never exceed the container's
|
|
231
|
+
// non-plugin remainder: with no sibling panel on that axis,
|
|
232
|
+
// `mpc − box` IS the exact chrome, while the frame-margin
|
|
233
|
+
// measurement overcounts (margins that overlap container
|
|
234
|
+
// padding) — the source of a ~2px target miss that pushed
|
|
235
|
+
// every commit past the plugin transport's ±0.5px resize
|
|
236
|
+
// dedupe, costing an extra corrective render. With siblings
|
|
237
|
+
// the remainder is large and the measured chrome wins.
|
|
238
|
+
let (cw, ch) = match &mpc {
|
|
239
|
+
Some(mpc) => (
|
|
240
|
+
cw.min((mpc.width() - plugin_box.width()).max(0.0)),
|
|
241
|
+
ch.min((mpc.height() - plugin_box.height()).max(0.0)),
|
|
242
|
+
),
|
|
243
|
+
None => (cw, ch),
|
|
244
|
+
};
|
|
245
|
+
|
|
225
246
|
let w = ((plugin_box.width() + cw) * width_ratio - cw).max(0.0);
|
|
226
247
|
let h = ((plugin_box.height() + ch) * height_ratio - ch).max(0.0);
|
|
227
248
|
Some((panel, w, h))
|
|
@@ -300,6 +321,24 @@ pub fn measure_settings_open_deltas(elem: &web_sys::HtmlElement) -> Option<(f64,
|
|
|
300
321
|
))
|
|
301
322
|
}
|
|
302
323
|
|
|
324
|
+
/// The signed `#main_panel_container` width delta a column-settings pin
|
|
325
|
+
/// toggle will cause: the docked `#modal_panel`'s flex box spans its drawer
|
|
326
|
+
/// child plus the overlapping divider (`drawer.right − divider.left`, the
|
|
327
|
+
/// same span in both modes since `.pinned` reparents nothing) — PINNING
|
|
328
|
+
/// takes it from the main column (positive, shrink), UNPINNING releases it
|
|
329
|
+
/// back (negative, grow). `None` when the drawer isn't mounted.
|
|
330
|
+
pub fn measure_column_settings_pin_delta(
|
|
331
|
+
elem: &web_sys::HtmlElement,
|
|
332
|
+
is_pinned: bool,
|
|
333
|
+
) -> Option<f64> {
|
|
334
|
+
let drawer = shadow_rect(elem, "#modal_panel > .split-panel-child:first-child")?;
|
|
335
|
+
let divider_left = shadow_rect(elem, "#modal_panel > .split-panel-divider")
|
|
336
|
+
.map(|r| r.left())
|
|
337
|
+
.unwrap_or_else(|| drawer.left());
|
|
338
|
+
let width = (drawer.right() - divider_left).max(0.0);
|
|
339
|
+
Some(if is_pinned { -width } else { width })
|
|
340
|
+
}
|
|
341
|
+
|
|
303
342
|
/// The frame chrome a plugin's grid cell has that the plugin itself doesn't
|
|
304
343
|
/// occupy, as `(width, height)` px: margin + border + titlebar. Measured live
|
|
305
344
|
/// from a `regular-layout-frame` and its plugin — robust to theme/CSS changes —
|