@perspective-dev/viewer 4.5.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective-viewer.js +20 -1
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/esm/extensions.d.ts +22 -3
- package/dist/esm/perspective-viewer.d.ts +9 -0
- package/dist/esm/perspective-viewer.inline.js +20 -1
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +20 -1
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/plugin.d.ts +71 -5
- package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
- package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
- package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
- package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
- package/dist/esm/ts-rs/Layout.d.ts +11 -0
- package/dist/esm/ts-rs/Orientation.d.ts +4 -0
- package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
- package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
- package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
- package/dist/wasm/perspective-viewer.d.ts +211 -77
- package/dist/wasm/perspective-viewer.js +528 -180
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
- package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_context_menu.css +14 -0
- package/src/css/column-settings-panel.css +1 -1
- package/src/css/containers/context-menu.css +101 -0
- package/src/css/panel-tab.css +193 -0
- package/src/css/plugin-selector.css +7 -10
- package/src/css/status-bar.css +75 -81
- package/src/css/viewer.css +180 -70
- package/src/rust/components/column_dropdown.rs +40 -30
- package/src/rust/components/column_selector/active_column.rs +7 -18
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
- package/src/rust/components/column_selector/config_selector.rs +33 -81
- package/src/rust/components/column_selector/filter_column.rs +5 -15
- package/src/rust/components/column_selector/inactive_column.rs +3 -8
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +5 -11
- package/src/rust/components/containers/split_panel.rs +175 -62
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/form/debug.rs +3 -0
- package/src/rust/components/global_filter_bar.rs +76 -0
- package/src/rust/components/main_panel/frame_theme.rs +186 -0
- package/src/rust/components/main_panel/msg.rs +62 -0
- package/src/rust/components/main_panel/presize.rs +220 -0
- package/src/rust/components/main_panel/reconcile.rs +224 -0
- package/src/rust/components/main_panel/render.rs +385 -0
- package/src/rust/components/main_panel/update.rs +184 -0
- package/src/rust/components/main_panel.rs +311 -71
- package/src/rust/components/mod.rs +4 -0
- package/src/rust/components/panel_menu.rs +486 -0
- package/src/rust/components/panel_tab.rs +505 -0
- package/src/rust/components/plugin_tab.rs +1 -1
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +110 -178
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/style/mod.rs +5 -0
- package/src/rust/components/style/style_provider.rs +2 -0
- package/src/rust/components/viewer/filters.rs +133 -0
- package/src/rust/components/viewer/msg.rs +180 -0
- package/src/rust/components/viewer/panels.rs +326 -0
- package/src/rust/components/viewer/render.rs +243 -0
- package/src/rust/components/viewer/settings.rs +402 -0
- package/src/rust/components/viewer/snapshots.rs +134 -0
- package/src/rust/components/viewer/wiring.rs +505 -0
- package/src/rust/components/viewer.rs +243 -756
- package/src/rust/config/export_method.rs +1 -1
- package/src/rust/config/mod.rs +4 -0
- package/src/rust/config/options.rs +71 -0
- package/src/rust/config/viewer_config.rs +24 -2
- package/src/rust/config/workspace_config.rs +92 -0
- package/src/rust/custom_elements/copy_dropdown.rs +3 -1
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +3 -1
- package/src/rust/custom_elements/viewer.rs +1015 -286
- package/src/rust/custom_events.rs +212 -61
- package/src/rust/js/mod.rs +2 -0
- package/src/rust/js/plugin.rs +297 -10
- package/src/rust/js/regular_layout.rs +258 -0
- package/src/rust/lib.rs +20 -1
- package/src/rust/presentation.rs +66 -33
- package/src/rust/queries/get_viewer_config.rs +29 -11
- package/src/rust/queries/plugin_column_styles.rs +2 -2
- package/src/rust/renderer/activate.rs +41 -14
- package/src/rust/renderer/dispatch.rs +436 -0
- package/src/rust/renderer/plugin_config.rs +495 -0
- package/src/rust/renderer/registry.rs +18 -6
- package/src/rust/renderer/render_timer.rs +29 -10
- package/src/rust/renderer.rs +350 -837
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +753 -264
- package/src/rust/tasks/apply_global_filters.rs +89 -0
- package/src/rust/tasks/auto_pause.rs +171 -0
- package/src/rust/tasks/copy_export.rs +29 -19
- package/src/rust/tasks/create_panel.rs +243 -0
- package/src/rust/tasks/dismiss_render_warning.rs +3 -1
- package/src/rust/tasks/edit_expression.rs +4 -4
- package/src/rust/tasks/eject.rs +64 -23
- package/src/rust/tasks/mod.rs +28 -4
- package/src/rust/tasks/pipeline.rs +411 -0
- package/src/rust/tasks/presize_panels.rs +324 -0
- package/src/rust/tasks/reset_all.rs +22 -12
- package/src/rust/tasks/resize_observer.rs +124 -87
- package/src/rust/tasks/restore_and_render.rs +80 -86
- package/src/rust/tasks/restore_panel.rs +145 -0
- package/src/rust/tasks/send_column_config.rs +5 -2
- package/src/rust/tasks/send_plugin_config.rs +5 -2
- package/src/rust/tasks/set_edit_mode.rs +59 -0
- package/src/rust/tasks/sync_update_panels.rs +152 -0
- package/src/rust/tasks/table_lifecycle.rs +158 -0
- package/src/rust/tasks/update_theme.rs +56 -12
- package/src/rust/utils/completion.rs +56 -0
- package/src/rust/utils/custom_element.rs +5 -0
- package/src/rust/utils/debounce.rs +130 -7
- package/src/rust/utils/mod.rs +6 -0
- package/src/rust/utils/pending_effects.rs +62 -0
- package/src/rust/utils/spawn.rs +33 -0
- package/src/rust/utils/weak_scope.rs +0 -10
- package/src/rust/workspace.rs +846 -0
- package/src/svg/active-indicator.svg +3 -0
- package/src/themes/botanical.css +5 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +6 -3
- package/src/themes/gruvbox-dark.css +5 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +4 -2
- package/src/themes/monokai.css +5 -2
- package/src/themes/phosphor.css +5 -2
- package/src/themes/pro-dark.css +5 -2
- package/src/themes/pro.css +2 -1
- package/src/themes/solarized-dark.css +5 -2
- package/src/themes/solarized.css +5 -2
- package/src/themes/vaporwave.css +5 -2
- package/src/ts/bootstrap.ts +8 -7
- package/src/ts/extensions.ts +30 -2
- package/src/ts/perspective-viewer.ts +10 -0
- package/src/ts/plugin.ts +72 -5
- package/src/ts/ts-rs/ClientOptions.ts +7 -0
- package/src/ts/ts-rs/ExportMethod.ts +3 -0
- package/src/ts/ts-rs/ExportOptions.ts +8 -0
- package/src/ts/ts-rs/GetClientOptions.ts +7 -0
- package/src/ts/ts-rs/GetTableOptions.ts +7 -0
- package/src/ts/ts-rs/Layout.ts +9 -0
- package/src/ts/ts-rs/Orientation.ts +6 -0
- package/src/ts/ts-rs/PanelOptions.ts +7 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
- package/src/ts/ts-rs/SplitLayout.ts +5 -0
- package/src/ts/ts-rs/TabLayout.ts +11 -0
- package/src/ts/ts-rs/ViewerConfig.ts +6 -2
- package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
- package/src/rust/tasks/intersection_observer.rs +0 -98
- package/src/rust/tasks/update_and_render.rs +0 -110
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
package/src/rust/tasks/eject.rs
CHANGED
|
@@ -10,35 +10,76 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
+
use futures::future::join_all;
|
|
13
14
|
use perspective_client::clone;
|
|
15
|
+
use perspective_js::utils::{ApiError, ApiResult};
|
|
14
16
|
use yew::Component;
|
|
15
17
|
|
|
16
18
|
use crate::ApiFuture;
|
|
17
|
-
use crate::renderer::Renderer;
|
|
18
19
|
use crate::root::Root;
|
|
19
|
-
use crate::session::{ResetOptions,
|
|
20
|
+
use crate::session::{ResetOptions, TableIntermediateState};
|
|
21
|
+
use crate::workspace::Workspace;
|
|
20
22
|
|
|
21
|
-
/// Tear down the entire viewer:
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
root
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
/// Tear down the entire viewer: dispose EVERY panel's engines (emit
|
|
24
|
+
/// `table_unloaded`, destroy the renderer under its own draw lock, eject the
|
|
25
|
+
/// session), then drop the Yew root once. Fans out over all panels — not just
|
|
26
|
+
/// the seed — so panels added via `addPanel` or a whole-element `restore` don't
|
|
27
|
+
/// leak their `View`/`Table` + plugin when the element is deleted or ejected.
|
|
28
|
+
pub fn delete_all<T: Component>(workspace: &Workspace, root: &Root<T>) -> ApiFuture<()> {
|
|
29
|
+
clone!(workspace, root);
|
|
30
|
+
ApiFuture::new(async move {
|
|
31
|
+
let panels = workspace
|
|
32
|
+
.take_reserved()
|
|
33
|
+
.into_iter()
|
|
34
|
+
.chain(
|
|
35
|
+
workspace
|
|
36
|
+
.panel_ids()
|
|
37
|
+
.into_iter()
|
|
38
|
+
.filter_map(|id| workspace.panel(&id)),
|
|
39
|
+
)
|
|
40
|
+
.collect::<Vec<_>>();
|
|
41
|
+
|
|
42
|
+
// Each panel owns its own `Renderer`/draw lock, so dispose them
|
|
43
|
+
// concurrently. `renderer.delete()` must hold the draw lock (it runs
|
|
44
|
+
// `plugin.delete()` synchronously), hence the per-panel `with_lock`.
|
|
45
|
+
// BEST-EFFORT teardown: attempt EVERY panel and ALWAYS destroy the
|
|
46
|
+
// Yew root, then report the first error. Aborting on the first
|
|
47
|
+
// failed panel (the previous behavior) leaked the root and every
|
|
48
|
+
// remaining panel's `View`/plugin.
|
|
49
|
+
let results = join_all(panels.iter().map(|panel| {
|
|
50
|
+
clone!(panel.session, panel.renderer);
|
|
51
|
+
session.table_unloaded.emit(false);
|
|
52
|
+
let was_errored = session.is_errored();
|
|
53
|
+
renderer.clone().with_lock(async move {
|
|
54
|
+
renderer.delete()?;
|
|
55
|
+
let reset = session
|
|
56
|
+
.reset(ResetOptions {
|
|
57
|
+
config: true,
|
|
58
|
+
expressions: true,
|
|
59
|
+
table: Some(TableIntermediateState::Ejected),
|
|
60
|
+
..ResetOptions::default()
|
|
61
|
+
})
|
|
62
|
+
.await;
|
|
63
|
+
|
|
64
|
+
match reset {
|
|
65
|
+
Err(_) if was_errored => Ok(()),
|
|
66
|
+
other => other,
|
|
67
|
+
}
|
|
40
68
|
})
|
|
41
|
-
|
|
69
|
+
}))
|
|
70
|
+
.await;
|
|
71
|
+
|
|
72
|
+
// Drop the Yew root once, after every panel's teardown was
|
|
73
|
+
// attempted — UNCONDITIONALLY, before error propagation, so a failed
|
|
74
|
+
// panel can't leak it.
|
|
75
|
+
let root_result = root
|
|
76
|
+
.borrow_mut()
|
|
77
|
+
.take()
|
|
78
|
+
.ok_or_else(|| ApiError::from("Already deleted"))
|
|
79
|
+
.map(|x| x.destroy());
|
|
80
|
+
|
|
81
|
+
results.into_iter().collect::<ApiResult<Vec<_>>>()?;
|
|
82
|
+
root_result?;
|
|
42
83
|
Ok(())
|
|
43
|
-
})
|
|
84
|
+
})
|
|
44
85
|
}
|
package/src/rust/tasks/mod.rs
CHANGED
|
@@ -21,30 +21,54 @@
|
|
|
21
21
|
//! [`Renderer`]: crate::renderer::Renderer
|
|
22
22
|
//! [`Presentation`]: crate::presentation::Presentation
|
|
23
23
|
|
|
24
|
+
mod apply_global_filters;
|
|
25
|
+
mod auto_pause;
|
|
24
26
|
mod copy_export;
|
|
27
|
+
mod create_panel;
|
|
25
28
|
mod dismiss_render_warning;
|
|
26
29
|
mod edit_expression;
|
|
27
30
|
mod eject;
|
|
28
|
-
mod
|
|
31
|
+
mod pipeline;
|
|
32
|
+
mod presize_panels;
|
|
29
33
|
mod reset_all;
|
|
30
34
|
mod resize_observer;
|
|
31
35
|
mod restore_and_render;
|
|
36
|
+
mod restore_panel;
|
|
32
37
|
mod send_column_config;
|
|
33
38
|
mod send_plugin_config;
|
|
34
|
-
mod
|
|
39
|
+
mod set_edit_mode;
|
|
40
|
+
mod sync_update_panels;
|
|
41
|
+
mod table_lifecycle;
|
|
35
42
|
mod update_theme;
|
|
36
43
|
mod validate_expression;
|
|
37
44
|
|
|
45
|
+
/// How long staged work (a hidden first draw, a presize sweep) may withhold
|
|
46
|
+
/// a layout transition before it is released anyway — the progressive-reveal
|
|
47
|
+
/// fallback for slow (e.g. remote) tables.
|
|
48
|
+
pub(crate) const STAGING_DEADLINE_MS: i32 = 500;
|
|
49
|
+
|
|
50
|
+
/// Fallback panel chrome `(width, height)` px (margin + border + titlebar)
|
|
51
|
+
/// when no frame is available to measure live (see
|
|
52
|
+
/// [`presize_panels::plugin_chrome`]).
|
|
53
|
+
pub(crate) const CHROME_FALLBACK: (f64, f64) = (8.0, 33.0);
|
|
54
|
+
|
|
55
|
+
pub use self::apply_global_filters::*;
|
|
56
|
+
pub use self::auto_pause::*;
|
|
38
57
|
pub use self::copy_export::*;
|
|
58
|
+
pub(crate) use self::create_panel::*;
|
|
39
59
|
pub use self::dismiss_render_warning::*;
|
|
40
60
|
pub use self::edit_expression::*;
|
|
41
61
|
pub use self::eject::*;
|
|
42
|
-
pub use self::
|
|
62
|
+
pub use self::pipeline::*;
|
|
63
|
+
pub use self::presize_panels::*;
|
|
43
64
|
pub use self::reset_all::*;
|
|
44
65
|
pub use self::resize_observer::*;
|
|
45
66
|
pub use self::restore_and_render::*;
|
|
67
|
+
pub(crate) use self::restore_panel::*;
|
|
46
68
|
pub use self::send_column_config::*;
|
|
47
69
|
pub use self::send_plugin_config::*;
|
|
48
|
-
pub use self::
|
|
70
|
+
pub use self::set_edit_mode::*;
|
|
71
|
+
pub(crate) use self::sync_update_panels::*;
|
|
72
|
+
pub(crate) use self::table_lifecycle::*;
|
|
49
73
|
pub use self::update_theme::*;
|
|
50
74
|
pub use self::validate_expression::*;
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
//! The render pipeline (see `SESSION_CONFIG_COHERENCE_PLAN.md`) — the ONE
|
|
14
|
+
//! review surface for how config-driven renders happen.
|
|
15
|
+
//!
|
|
16
|
+
//! Invariants enforced here and by the types this composes:
|
|
17
|
+
//! - **I1** config mutation is a synchronous commit
|
|
18
|
+
//! ([`Session::commit_view_config`]) — no async writer exists.
|
|
19
|
+
//! - **I2** every run consumes an immutable snapshot taken inside the draw
|
|
20
|
+
//! lock; async stages never re-read the live config (the `ValidatedSnapshot`
|
|
21
|
+
//! type-state token).
|
|
22
|
+
//! - **I3** runs serialize on the per-`Renderer` draw lock (the [`RenderGuard`]
|
|
23
|
+
//! witness — plugin dispatch without it does not compile) and every commit
|
|
24
|
+
//! schedules a run, so the final render always reflects the final commit.
|
|
25
|
+
//! - **I5** the run pins a [`RenderContext`] so plugin read-backs mid-render
|
|
26
|
+
//! answer from the run's snapshot, never live state.
|
|
27
|
+
|
|
28
|
+
use std::rc::Rc;
|
|
29
|
+
|
|
30
|
+
use futures::future::LocalBoxFuture;
|
|
31
|
+
use perspective_client::config::ViewConfigUpdate;
|
|
32
|
+
use perspective_client::{View, clone};
|
|
33
|
+
use perspective_js::utils::*;
|
|
34
|
+
use wasm_bindgen::JsValue;
|
|
35
|
+
use yew::prelude::*;
|
|
36
|
+
|
|
37
|
+
use crate::config::{ColumnConfigUpdate, PluginConfigUpdate};
|
|
38
|
+
use crate::presentation::Presentation;
|
|
39
|
+
use crate::renderer::{RenderContext, Renderer};
|
|
40
|
+
use crate::session::{BindDisposition, Session};
|
|
41
|
+
use crate::utils::RenderGuard;
|
|
42
|
+
|
|
43
|
+
/// Snapshot → validate → bind → cache + pin the [`RenderContext`]. The core
|
|
44
|
+
/// of every config-driven run. Returns HOW the bind reconciled (the
|
|
45
|
+
/// [`BindDisposition`], whose `Rebuilt` arm alone carries the `plugin.draw`
|
|
46
|
+
/// witness — see [`dispatch_bound`]) and the RAII context pin (dropped —
|
|
47
|
+
/// unpinning — when the caller's locked task ends).
|
|
48
|
+
pub async fn bind_snapshot(
|
|
49
|
+
guard: &RenderGuard,
|
|
50
|
+
session: &Session,
|
|
51
|
+
renderer: &Renderer,
|
|
52
|
+
) -> ApiResult<(BindDisposition, Option<crate::renderer::ContextPin>)> {
|
|
53
|
+
// A `load()` is mid-classification (see [`Session::pending_load`]): DEFER
|
|
54
|
+
// rather than bind the incoming config against the still-bound outgoing
|
|
55
|
+
// table. Holds the last painted frame; the `load()` run performs the one
|
|
56
|
+
// reconciling bind once its payload resolves — the single visible
|
|
57
|
+
// transition, correct in whichever future (Table/Client) materializes.
|
|
58
|
+
if session.has_pending_load() {
|
|
59
|
+
return Ok((BindDisposition::Deferred, None));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let snap = session.snapshot(guard);
|
|
63
|
+
let validated = session.validate_snapshot(guard, snap).await?;
|
|
64
|
+
let disposition = session.bind_view(guard, validated).await?;
|
|
65
|
+
let pin = if let Some(view) = disposition.view() {
|
|
66
|
+
let ctx = Rc::new(build_render_context(session, renderer, view)?);
|
|
67
|
+
renderer.set_cached_context(ctx.clone());
|
|
68
|
+
Some(renderer.pin_context(guard, ctx))
|
|
69
|
+
} else {
|
|
70
|
+
None
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
Ok((disposition, pin))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Who initiated a render run — threaded EXPLICITLY from the closed set of
|
|
77
|
+
/// `#[wasm_bindgen]` element entry points, never inferred from lock state
|
|
78
|
+
/// or timing (a temporal "is this nested?" heuristic would misclassify
|
|
79
|
+
/// legitimate concurrent public calls).
|
|
80
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
81
|
+
pub enum RunOrigin {
|
|
82
|
+
/// A public element API entry (`restore`, `restorePanel`, `reset`,
|
|
83
|
+
/// `load`, …): an explicit external request, so a run that reconciles
|
|
84
|
+
/// `Unchanged` with no state delta STILL repaints — the documented
|
|
85
|
+
/// no-op-restore refresh affordance (`update` source 6, "the user
|
|
86
|
+
/// asked" — `PLUGIN_DRAW_INVARIANT_PLAN.md` amendment).
|
|
87
|
+
Public,
|
|
88
|
+
/// A host-initiated run (UI commits, unpause resumes, subscriptions):
|
|
89
|
+
/// a reconciled no-op dispatches nothing.
|
|
90
|
+
Internal,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The ONE decision table mapping a run's [`BindDisposition`] to its plugin
|
|
94
|
+
/// dispatch (`PLUGIN_DRAW_INVARIANT_PLAN.md`, as amended 2026-07-16):
|
|
95
|
+
/// `plugin.draw` fires iff there is a NEW `View` — a REBUILD, or a
|
|
96
|
+
/// freshly-selected plugin's first paint of the bound `View`
|
|
97
|
+
/// (`promote_first_paint`). `plugin.update` fires iff the `View` is
|
|
98
|
+
/// unchanged but a plugin-visible SOURCE changed: the `Adopted`
|
|
99
|
+
/// config delta, a genuinely changed plugin/columns bucket this run
|
|
100
|
+
/// delivered via `plugin.restore` (`plugin_state_changed`), or an explicit
|
|
101
|
+
/// public API request ([`RunOrigin::Public`]). An INTERNAL run that
|
|
102
|
+
/// reconciled `Unchanged` with no state delta dispatches NOTHING — no
|
|
103
|
+
/// defensive repaints (this is what makes an inert plugin echo, e.g. the
|
|
104
|
+
/// datagrid's `toggle_edit_mode` `restorePanel`, render-silent). Deferred
|
|
105
|
+
/// binds dispatch nothing.
|
|
106
|
+
pub async fn dispatch_bound(
|
|
107
|
+
guard: &RenderGuard,
|
|
108
|
+
renderer: &Renderer,
|
|
109
|
+
disposition: BindDisposition,
|
|
110
|
+
plugin_state_changed: bool,
|
|
111
|
+
origin: RunOrigin,
|
|
112
|
+
) -> ApiResult<()> {
|
|
113
|
+
match disposition {
|
|
114
|
+
BindDisposition::Rebuilt(fresh) => renderer.draw_fresh(guard, fresh).await,
|
|
115
|
+
BindDisposition::Adopted(view) => match renderer.promote_first_paint(&view) {
|
|
116
|
+
Some(fresh) => renderer.draw_fresh(guard, fresh).await,
|
|
117
|
+
// The adoption's effective-config delta IS the source.
|
|
118
|
+
None => renderer.update_bound(guard, &view).await,
|
|
119
|
+
},
|
|
120
|
+
BindDisposition::Unchanged(view) => match renderer.promote_first_paint(&view) {
|
|
121
|
+
Some(fresh) => renderer.draw_fresh(guard, fresh).await,
|
|
122
|
+
None if plugin_state_changed || origin == RunOrigin::Public => {
|
|
123
|
+
renderer.update_bound(guard, &view).await
|
|
124
|
+
},
|
|
125
|
+
None => {
|
|
126
|
+
tracing::debug!("Render skipped, reconciled no-op (internal)");
|
|
127
|
+
Ok(())
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
BindDisposition::Deferred => {
|
|
131
|
+
tracing::debug!("Render skipped, no `View` attached");
|
|
132
|
+
Ok(())
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Assemble the plugin-visible state bundle for the just-bound `View` —
|
|
138
|
+
/// every value a plugin may read back during its render, all belonging to
|
|
139
|
+
/// the same snapshot (I5).
|
|
140
|
+
fn build_render_context(
|
|
141
|
+
session: &Session,
|
|
142
|
+
renderer: &Renderer,
|
|
143
|
+
view: &View,
|
|
144
|
+
) -> ApiResult<RenderContext> {
|
|
145
|
+
Ok(RenderContext {
|
|
146
|
+
view_config: session
|
|
147
|
+
.get_rendered_view_config()
|
|
148
|
+
.ok_or("No bound `View`")?,
|
|
149
|
+
view: view.clone(),
|
|
150
|
+
table: session.get_table().ok_or("No `Table` set")?,
|
|
151
|
+
client: session.get_client().ok_or("No `Client` set")?,
|
|
152
|
+
edit_port: session.metadata().get_edit_port(),
|
|
153
|
+
theme: renderer.theme(),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Commit `update` (sync, may `Err` before any state changes — I4) and
|
|
158
|
+
/// schedule a locked run. The single entry point for every "change the
|
|
159
|
+
/// config and draw" action; returns the run's future (I6: public mutators
|
|
160
|
+
/// resolve at render completion).
|
|
161
|
+
pub fn apply_and_render(
|
|
162
|
+
session: &Session,
|
|
163
|
+
renderer: &Renderer,
|
|
164
|
+
update: ViewConfigUpdate,
|
|
165
|
+
) -> ApiResult<ApiFuture<()>> {
|
|
166
|
+
update_plugin_and_render(session, renderer, update, None)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// [`apply_and_render`] plus a plugin selection (resolved via
|
|
170
|
+
/// [`Renderer::resolve_plugin_update`]). The plugin swap is committed inside
|
|
171
|
+
/// the locked run, atomically with the view rebind — never staged on the
|
|
172
|
+
/// `Renderer` where a concurrent draw could observe it.
|
|
173
|
+
pub fn update_plugin_and_render(
|
|
174
|
+
session: &Session,
|
|
175
|
+
renderer: &Renderer,
|
|
176
|
+
update: ViewConfigUpdate,
|
|
177
|
+
plugin_idx: Option<usize>,
|
|
178
|
+
) -> ApiResult<ApiFuture<()>> {
|
|
179
|
+
session.commit_view_config(update)?;
|
|
180
|
+
// Spinner accounting (RAII): created only when the commit succeeded,
|
|
181
|
+
// moved into the run future, settled on every exit path by `Drop`.
|
|
182
|
+
let run_token = session.begin_config_run();
|
|
183
|
+
clone!(session, renderer);
|
|
184
|
+
Ok(ApiFuture::new(async move {
|
|
185
|
+
let _run_token = run_token;
|
|
186
|
+
render_run(session, renderer, plugin_idx).await
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Re-render from the current commit without applying an update.
|
|
191
|
+
pub fn just_render(session: &Session, renderer: &Renderer) -> ApiResult<ApiFuture<()>> {
|
|
192
|
+
clone!(session, renderer);
|
|
193
|
+
Ok(ApiFuture::new(async move {
|
|
194
|
+
render_run(session, renderer, None).await
|
|
195
|
+
}))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Everything that varies between locked render runs, consumed by
|
|
199
|
+
/// [`locked_run`] — the ONE lock-body composition. `Default` is the
|
|
200
|
+
/// host-internal UI-commit run: `Internal` origin, no plugin swap, no
|
|
201
|
+
/// pre-bind task, no bucket updates, no dispatch visibility gate.
|
|
202
|
+
pub(crate) struct RunSpec {
|
|
203
|
+
/// Who initiated this run (see [`RunOrigin`]).
|
|
204
|
+
pub origin: RunOrigin,
|
|
205
|
+
|
|
206
|
+
/// A plugin swap resolved by [`Renderer::resolve_plugin_update`],
|
|
207
|
+
/// committed inside the run atomically with the rebind.
|
|
208
|
+
pub plugin_idx: Option<usize>,
|
|
209
|
+
|
|
210
|
+
/// Plugin-level bucket update, applied inside the run after the bind
|
|
211
|
+
/// (so strip-on-write sees fresh schemas).
|
|
212
|
+
pub plugin_config: PluginConfigUpdate,
|
|
213
|
+
|
|
214
|
+
/// Per-column bucket update; same timing as `plugin_config`.
|
|
215
|
+
pub columns_config: ColumnConfigUpdate,
|
|
216
|
+
|
|
217
|
+
/// Pre-bind hook awaited inside the lock (table binding, resets) —
|
|
218
|
+
/// BEFORE the error guard, so a task that recovers the session (e.g.
|
|
219
|
+
/// `restorePanel`'s errored-recovery reset) unblocks its own run.
|
|
220
|
+
pub task: Option<LocalBoxFuture<'static, ApiResult<()>>>,
|
|
221
|
+
|
|
222
|
+
/// When set, plugin dispatch is skipped while the host element is not
|
|
223
|
+
/// visible (`Presentation::is_visible`) — the `restore` family's gate
|
|
224
|
+
/// (the eventual auto-pause resume rebuilds and redraws). `None`
|
|
225
|
+
/// dispatches unconditionally (UI-commit runs, whose callers gate on
|
|
226
|
+
/// drawn/loaded state themselves).
|
|
227
|
+
pub presentation: Option<Presentation>,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
impl Default for RunSpec {
|
|
231
|
+
fn default() -> Self {
|
|
232
|
+
Self {
|
|
233
|
+
origin: RunOrigin::Internal,
|
|
234
|
+
plugin_idx: None,
|
|
235
|
+
plugin_config: PluginConfigUpdate::Missing,
|
|
236
|
+
columns_config: ColumnConfigUpdate::Missing,
|
|
237
|
+
task: None,
|
|
238
|
+
presentation: None,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// One locked, witnessed, snapshot-consuming render run — the SINGLE lock
|
|
244
|
+
/// body shared by every config-driven run (`apply_and_render` &co. via
|
|
245
|
+
/// [`render_run`]'s tail, the `restore` family via
|
|
246
|
+
/// [`super::restore_and_render`]): eager mount → pre-bind `task` →
|
|
247
|
+
/// plugin-swap commit → theme stamp → error guard → [`bind_snapshot`]
|
|
248
|
+
/// (gated on a bound `Table`, else `Deferred` — the config is already
|
|
249
|
+
/// committed, so the eventual `load()` run binds from it) → bucket updates
|
|
250
|
+
/// + materialized `plugin.restore` → [`dispatch_bound`].
|
|
251
|
+
///
|
|
252
|
+
/// A pre-existing session error skips the run for `Internal` origins and
|
|
253
|
+
/// fails it for `Public` ones (the caller asked and must hear the failure —
|
|
254
|
+
/// I6). Returns the RAW run result; error-reporting policy (`set_run_error`
|
|
255
|
+
/// vs. propagate) belongs to the caller.
|
|
256
|
+
pub(crate) async fn locked_run(
|
|
257
|
+
session: &Session,
|
|
258
|
+
renderer: &Renderer,
|
|
259
|
+
spec: RunSpec,
|
|
260
|
+
) -> ApiResult<()> {
|
|
261
|
+
clone!(session, renderer);
|
|
262
|
+
renderer
|
|
263
|
+
.clone()
|
|
264
|
+
.render_task(|guard| async move {
|
|
265
|
+
renderer.mount_active_plugin()?;
|
|
266
|
+
if let Some(task) = spec.task {
|
|
267
|
+
task.await?;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let plugin_swapped = renderer.commit_plugin(spec.plugin_idx)?;
|
|
271
|
+
let plugin = renderer.active_plugin().ok_or("No Plugin")?;
|
|
272
|
+
renderer.stamp_theme(Some(&plugin));
|
|
273
|
+
if let Some(error) = session.get_error() {
|
|
274
|
+
return match spec.origin {
|
|
275
|
+
RunOrigin::Public => Err(error),
|
|
276
|
+
RunOrigin::Internal => Ok(()),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let (disposition, _pin) = if session.get_table().is_some() {
|
|
281
|
+
bind_snapshot(&guard, &session, &renderer).await?
|
|
282
|
+
} else {
|
|
283
|
+
(BindDisposition::Deferred, None)
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
let view_config_snapshot = session.get_view_config().clone();
|
|
287
|
+
let plugin_config_changed =
|
|
288
|
+
renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config);
|
|
289
|
+
let columns_config_changed = renderer.update_columns_configs(
|
|
290
|
+
&view_config_snapshot,
|
|
291
|
+
&session,
|
|
292
|
+
spec.columns_config,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
let changed = plugin_config_changed || columns_config_changed;
|
|
296
|
+
if changed || plugin_swapped {
|
|
297
|
+
let plugin_config_snapshot = renderer.get_plugin_config();
|
|
298
|
+
let plugin_update =
|
|
299
|
+
JsValue::from_serde_ext(&plugin_config_snapshot).unwrap_or(JsValue::NULL);
|
|
300
|
+
let columns_config = renderer
|
|
301
|
+
.all_columns_configs_materialized(&view_config_snapshot, &session)
|
|
302
|
+
.await;
|
|
303
|
+
plugin.restore(&plugin_update, Some(&columns_config))?;
|
|
304
|
+
if plugin_config_changed {
|
|
305
|
+
renderer.plugin_config_changed.emit(plugin_config_snapshot);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if spec
|
|
310
|
+
.presentation
|
|
311
|
+
.as_ref()
|
|
312
|
+
.map(|p| p.is_visible())
|
|
313
|
+
.unwrap_or(true)
|
|
314
|
+
{
|
|
315
|
+
dispatch_bound(&guard, &renderer, disposition, changed, spec.origin).await?;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
Ok(())
|
|
319
|
+
})
|
|
320
|
+
.await
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// [`locked_run`]'s host-internal tail: a failed RUN sets error state (with
|
|
324
|
+
/// the reset-reconnect affordance); the committed config is NOT rolled back
|
|
325
|
+
/// (I4). Cancellation by a superseding run ("View already deleted") is not
|
|
326
|
+
/// a failure.
|
|
327
|
+
async fn render_run(
|
|
328
|
+
session: Session,
|
|
329
|
+
renderer: Renderer,
|
|
330
|
+
plugin_idx: Option<usize>,
|
|
331
|
+
) -> ApiResult<()> {
|
|
332
|
+
let spec = RunSpec {
|
|
333
|
+
plugin_idx,
|
|
334
|
+
..RunSpec::default()
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
match locked_run(&session, &renderer, spec)
|
|
338
|
+
.await
|
|
339
|
+
.ignore_view_delete()
|
|
340
|
+
{
|
|
341
|
+
Err(e) => session.set_run_error(e).await,
|
|
342
|
+
Ok(_) => Ok(()),
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// One TRANSACTIONAL activation repaint (see the I5 audit gap in
|
|
347
|
+
/// `SESSION_CONFIG_COHERENCE_PLAN.md` §4): under the panel's draw lock,
|
|
348
|
+
/// stamp the `active` class + theme and `plugin.resize()` in ONE dispatch,
|
|
349
|
+
/// so the activation-dependent chrome (e.g. the datagrid's edit
|
|
350
|
+
/// column-header row, decided by its style listener reading that class) and
|
|
351
|
+
/// the class that styles it land in a single paint commit, never split.
|
|
352
|
+
///
|
|
353
|
+
/// `resize`, NOT `draw`/`update`: activation creates no new `View` and
|
|
354
|
+
/// changes no data (`plugin.draw` ⇔ new `View` —
|
|
355
|
+
/// `PLUGIN_DRAW_INVARIANT_PLAN.md`), and a full dispatch on charts is a
|
|
356
|
+
/// fetch + multi-blit repaint — the stacked-tab two-stage regression, paid
|
|
357
|
+
/// by BOTH sides of every tab switch. `resize` is each plugin's cheap
|
|
358
|
+
/// repaint-from-retained-state, and both built-ins skip it while hidden, so
|
|
359
|
+
/// the OUTGOING (unslotted) panel's nudge costs nothing. Deliberately NOT
|
|
360
|
+
/// debounced/throttled: activation is a one-shot interaction, not a data
|
|
361
|
+
/// stream, and the throttle timer is a task boundary the browser paints
|
|
362
|
+
/// across.
|
|
363
|
+
///
|
|
364
|
+
/// EXCEPTION: a panel whose `table_updated` redraws were dropped while it
|
|
365
|
+
/// was hidden ([`Renderer::take_data_stale`]) HAS changed data — its
|
|
366
|
+
/// retained frame is stale, so this activation dispatches one full
|
|
367
|
+
/// `plugin.update` (same `View`, fresh data; `draw` stays reserved for new
|
|
368
|
+
/// `View`s) instead of the chrome-only repaint. `draw_view` stamps the
|
|
369
|
+
/// activation class + theme itself, so the atomicity contract above holds
|
|
370
|
+
/// on this branch too.
|
|
371
|
+
pub async fn activation_render(session: Session, renderer: Renderer) -> ApiResult<()> {
|
|
372
|
+
let result = {
|
|
373
|
+
clone!(session, renderer);
|
|
374
|
+
renderer
|
|
375
|
+
.clone()
|
|
376
|
+
.render_task(|guard| async move {
|
|
377
|
+
match session.get_view() {
|
|
378
|
+
Some(view) => {
|
|
379
|
+
let _pin = renderer
|
|
380
|
+
.cached_context()
|
|
381
|
+
.map(|ctx| renderer.pin_context(&guard, ctx));
|
|
382
|
+
if renderer.take_data_stale() {
|
|
383
|
+
renderer.update_bound(&guard, &view).await
|
|
384
|
+
} else {
|
|
385
|
+
renderer.activation_repaint(&guard).await
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
None => Ok(()),
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
.await
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
result.ignore_view_delete().map(|_| ())
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// Create a [`Callback`] that resizes from the current `View` and `Plugin`,
|
|
398
|
+
/// or runs a full render when the plugin has never drawn.
|
|
399
|
+
pub fn resize_callback(session: &Session, renderer: &Renderer) -> Callback<()> {
|
|
400
|
+
clone!(session, renderer);
|
|
401
|
+
Callback::from(move |_| {
|
|
402
|
+
clone!(renderer, session);
|
|
403
|
+
crate::utils::spawn_owned("resize-callback", async move {
|
|
404
|
+
if !renderer.is_plugin_activated()? {
|
|
405
|
+
just_render(&session, &renderer)?.await
|
|
406
|
+
} else {
|
|
407
|
+
renderer.resize().await
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
})
|
|
411
|
+
}
|