@perspective-dev/viewer 4.5.1 → 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.cdn.d.ts +3 -0
- package/dist/esm/perspective-viewer.d.ts +9 -0
- package/dist/esm/perspective-viewer.inline.d.ts +3 -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/OnUpdateData.d.ts +6 -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/ViewOnUpdateResp.d.ts +4 -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 +213 -79
- package/dist/wasm/perspective-viewer.js +537 -189
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +36 -28
- package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
- package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
- package/package.json +3 -2
- package/src/css/_column_dropdown.css +14 -0
- package/src/css/_context_menu.css +14 -0
- package/src/css/_dropdown_menu.css +14 -0
- package/src/css/_filter_dropdown.css +14 -0
- package/src/css/_function_dropdown.css +14 -0
- package/src/css/_viewer_bundle.css +41 -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 +181 -71
- package/src/rust/components/column_dropdown.rs +43 -33
- package/src/rust/components/column_selector/active_column.rs +7 -18
- package/src/rust/components/column_selector/aggregate_selector.rs +4 -10
- package/src/rust/components/column_selector/config_selector.rs +33 -84
- package/src/rust/components/column_selector/empty_column.rs +0 -3
- package/src/rust/components/column_selector/filter_column.rs +5 -18
- package/src/rust/components/column_selector/inactive_column.rs +3 -8
- package/src/rust/components/column_selector/invalid_column.rs +0 -4
- package/src/rust/components/column_selector/sort_column.rs +3 -6
- package/src/rust/components/column_selector.rs +5 -14
- package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +0 -3
- package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +0 -3
- package/src/rust/components/column_settings_sidebar.rs +0 -3
- package/src/rust/components/containers/dropdown_menu.rs +1 -3
- package/src/rust/components/containers/scroll_panel.rs +1 -3
- package/src/rust/components/containers/split_panel.rs +175 -66
- package/src/rust/components/containers/tab_list.rs +0 -3
- package/src/rust/components/context_menu.rs +130 -0
- package/src/rust/components/datetime_column_style.rs +0 -3
- package/src/rust/components/empty_row.rs +0 -3
- package/src/rust/components/expression_editor.rs +2 -3
- package/src/rust/components/filter_dropdown.rs +3 -3
- package/src/rust/components/font_loader.rs +2 -2
- package/src/rust/components/form/code_editor.rs +0 -3
- package/src/rust/components/form/debug.rs +3 -4
- package/src/rust/components/function_dropdown.rs +3 -3
- 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/number_series_style.rs +0 -3
- package/src/rust/components/panel_menu.rs +486 -0
- package/src/rust/components/panel_tab.rs +505 -0
- package/src/rust/components/plugin_selector.rs +0 -3
- package/src/rust/components/plugin_tab.rs +1 -6
- package/src/rust/components/portal.rs +5 -2
- package/src/rust/components/render_warning.rs +0 -3
- package/src/rust/components/settings_panel.rs +13 -5
- package/src/rust/components/status_bar.rs +113 -180
- package/src/rust/components/status_indicator.rs +6 -9
- package/src/rust/components/string_column_style.rs +0 -3
- package/src/rust/components/style/mod.rs +65 -27
- package/src/rust/components/style/style_provider.rs +51 -16
- package/src/rust/components/style_controls/number_string_format.rs +1 -8
- package/src/rust/components/type_icon.rs +1 -4
- 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 -758
- 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 +9 -3
- package/src/rust/custom_elements/debug_plugin.rs +20 -1
- package/src/rust/custom_elements/export_dropdown.rs +9 -3
- 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 -2
- 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 -827
- package/src/rust/session/column_defaults_update.rs +26 -8
- package/src/rust/session/metadata.rs +11 -1
- package/src/rust/session/view_subscription.rs +30 -15
- package/src/rust/session.rs +753 -251
- 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/{components/style/local_style.rs → utils/spawn.rs} +17 -19
- 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 +7 -2
- package/src/themes/defaults.css +54 -2
- package/src/themes/dracula.css +8 -3
- package/src/themes/gruvbox-dark.css +7 -2
- package/src/themes/gruvbox.css +5 -2
- package/src/themes/icons.css +4 -2
- package/src/themes/monokai.css +7 -2
- package/src/themes/phosphor.css +7 -2
- package/src/themes/pro-dark.css +7 -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 +7 -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/tsconfig.json +1 -1
- package/src/rust/components/style/style_cache.rs +0 -136
- 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-3cd58f0374935772/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-3cd58f0374935772/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
package/src/rust/js/plugin.rs
CHANGED
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
+
use std::cell::RefCell;
|
|
14
|
+
use std::collections::HashMap;
|
|
15
|
+
|
|
13
16
|
use perspective_js::JsViewWindow;
|
|
14
17
|
use perspective_js::utils::*;
|
|
18
|
+
use wasm_bindgen::JsCast;
|
|
15
19
|
use wasm_bindgen::prelude::*;
|
|
16
20
|
|
|
17
21
|
use crate::config::PluginStaticConfig;
|
|
@@ -25,6 +29,93 @@ extern "C" {
|
|
|
25
29
|
#[derive(Clone)]
|
|
26
30
|
pub type JsPerspectiveViewer;
|
|
27
31
|
|
|
32
|
+
/// A `<perspective-viewer>` plugin custom element.
|
|
33
|
+
///
|
|
34
|
+
/// # Capability tiers
|
|
35
|
+
///
|
|
36
|
+
/// Only `get_static_config`, `draw`, `save`/`restore` and `delete` are
|
|
37
|
+
/// REQUIRED. Every other method is optional — detected once per element
|
|
38
|
+
/// tag ([`PluginCapabilities`], methods live on the class prototype) —
|
|
39
|
+
/// with a defined host fallback, so a minimal plugin renders correctly
|
|
40
|
+
/// and each added method buys performance or polish, never correctness:
|
|
41
|
+
///
|
|
42
|
+
/// | method | host fallback when absent |
|
|
43
|
+
/// |------------|--------------------------------------------------------|
|
|
44
|
+
/// | `update` | `draw` — every repaint is a full render (such plugins |
|
|
45
|
+
/// | | must NOT treat `draw` as a state-reset license) |
|
|
46
|
+
/// | `resize` | no-op — CSS reflows the box; next dispatch repaints |
|
|
47
|
+
/// | `restyle` | no-op — the plugin re-reads CSS at its next render |
|
|
48
|
+
/// | `clear` | no-op |
|
|
49
|
+
/// | `deselect` | no-op |
|
|
50
|
+
/// | `presize` | held style-override presize (`Renderer` resize paths) |
|
|
51
|
+
/// | `render` | image export reports unsupported |
|
|
52
|
+
///
|
|
53
|
+
/// # Call discipline
|
|
54
|
+
///
|
|
55
|
+
/// Plugin implementations assume the host NEVER overlaps calls on the
|
|
56
|
+
/// same element: each of the rendering methods (`draw`, `update`,
|
|
57
|
+
/// `render`, `clear`, `resize`) and `delete` must run to completion
|
|
58
|
+
/// before the next begins. Every call site must therefore hold that
|
|
59
|
+
/// plugin's per-`Renderer` draw lock, witnessed by
|
|
60
|
+
/// [`crate::utils::RenderGuard`] — the guard-taking dispatch helpers
|
|
61
|
+
/// (`Renderer::draw_fresh`/`update_bound`,
|
|
62
|
+
/// `renderer::activate::activate_plugin`) and the already-locked
|
|
63
|
+
/// `render_task`/`update_lazy`/`resize`/`restyle_all`/`dispose`
|
|
64
|
+
/// entrypoints do not compile from an unlocked context. Synchronous
|
|
65
|
+
/// calls (`restore`, `restyle`, the schema queries) are exempt, though
|
|
66
|
+
/// `restyle` should ride inside a locked restyle-then-update sequence
|
|
67
|
+
/// (see `Renderer::restyle_all`).
|
|
68
|
+
///
|
|
69
|
+
/// # Dispatch semantics (`PLUGIN_DRAW_INVARIANT_PLAN.md`, amended
|
|
70
|
+
/// 2026-07-16)
|
|
71
|
+
///
|
|
72
|
+
/// The host invokes `draw` **iff there is a NEW `View` for this
|
|
73
|
+
/// plugin** — `bind_view` REBUILT the engine `View`, or a
|
|
74
|
+
/// freshly-selected plugin element owes its first paint of the bound
|
|
75
|
+
/// one — so implementations may treat `draw` as "new data shape"
|
|
76
|
+
/// (reset zoom/scroll/domain state). The witness enforcing this is
|
|
77
|
+
/// [`crate::session::FreshView`], which only the rebuild and
|
|
78
|
+
/// first-paint paths mint.
|
|
79
|
+
///
|
|
80
|
+
/// `update` is a repaint of the SAME `View`, invoked iff a
|
|
81
|
+
/// plugin-visible SOURCE changed — one of exactly six: (1) data
|
|
82
|
+
/// (`View::on_update`), (2) an adopted placeholder-config delta,
|
|
83
|
+
/// (3) a genuinely CHANGED `plugin_config`/`columns_config` the host
|
|
84
|
+
/// just delivered via `restore` (so `update`-after-`restore` is the
|
|
85
|
+
/// sanctioned delivery pairing), (4) genuinely changed CSS vars just
|
|
86
|
+
/// applied via `restyle` (`Renderer::restyle_all`, or the fused
|
|
87
|
+
/// stale-capture restyle immediately preceding a dispatch inside
|
|
88
|
+
/// `draw_view` — gated by `Renderer::needs_restyle`), (5) a
|
|
89
|
+
/// render-limits change (warning dismissed), (6) an explicit PUBLIC
|
|
90
|
+
/// element-API call that reconciled as a no-op
|
|
91
|
+
/// ([`crate::tasks::RunOrigin::Public`] — the documented
|
|
92
|
+
/// `viewer.restore({})` refresh affordance). An INTERNAL run that
|
|
93
|
+
/// changed none of these dispatches nothing.
|
|
94
|
+
///
|
|
95
|
+
/// `resize` is geometry/chrome only, from retained state (and is also
|
|
96
|
+
/// the activation-chrome nudge — implementations should no-op it
|
|
97
|
+
/// while hidden).
|
|
98
|
+
///
|
|
99
|
+
/// `restore`/`save` are state transfer, NOT rendering: plugins must
|
|
100
|
+
/// not render internally from them, and must not call host APIs from
|
|
101
|
+
/// inside `restore` (a `restore`-triggered `restorePanel` echo is how
|
|
102
|
+
/// the initial-load double render happened — user gestures may echo,
|
|
103
|
+
/// host-delivered restores must not). (The built-in `Debug` plugin's
|
|
104
|
+
/// own `resize()` delegating to its `draw()` is plugin-INTERNAL and
|
|
105
|
+
/// exempt.)
|
|
106
|
+
///
|
|
107
|
+
/// # Render-callable contract (invariant I5,
|
|
108
|
+
/// `SESSION_CONFIG_COHERENCE_PLAN.md`)
|
|
109
|
+
///
|
|
110
|
+
/// From inside `draw`/`update`/`render`/`resize`, a plugin may call
|
|
111
|
+
/// back into its host `<perspective-viewer>` ONLY the per-panel
|
|
112
|
+
/// snapshot getters — `getViewConfigPanel`, `getTablePanel`,
|
|
113
|
+
/// `getClientPanel`, `getEditPortPanel` — which answer from the run's
|
|
114
|
+
/// pinned `RenderContext`, so plugin-visible state always equals the
|
|
115
|
+
/// snapshot being drawn, never a fresher in-flight commit.
|
|
116
|
+
/// Lock-acquiring host methods (`savePanel`, `restorePanel`, `flush`,
|
|
117
|
+
/// `download`/`export`/`copy`, …) DEADLOCK on the non-reentrant draw
|
|
118
|
+
/// lock if called from a render and are forbidden.
|
|
28
119
|
#[derive(Clone)]
|
|
29
120
|
pub type JsPerspectiveViewerPlugin;
|
|
30
121
|
|
|
@@ -51,24 +142,41 @@ extern "C" {
|
|
|
51
142
|
#[wasm_bindgen(method, catch, js_name = plugin_config_schema)]
|
|
52
143
|
pub fn _plugin_config_schema(this: &JsPerspectiveViewerPlugin, view_config: &JsValue) -> ApiResult<JsValue>;
|
|
53
144
|
|
|
145
|
+
/// STATE TRANSFER, not rendering (dispatch semantics, rule for
|
|
146
|
+
/// `restore`/`save`): deliver a `plugin_config` + `columns_config`
|
|
147
|
+
/// snapshot into the plugin. Sync and lock-exempt. The host pairs a
|
|
148
|
+
/// restore that genuinely CHANGED plugin state with exactly one
|
|
149
|
+
/// `update` in the same locked run (update source 3) — plugins must
|
|
150
|
+
/// not render from `restore()` themselves, and must not call host
|
|
151
|
+
/// APIs from inside it (the echo rule: a plugin-issued `restorePanel`
|
|
152
|
+
/// re-enters through the PUBLIC surface, indistinguishable from a
|
|
153
|
+
/// user call — the initial-load double-render bug). Prefer the typed
|
|
154
|
+
/// [`JsPerspectiveViewerPlugin::restore`] wrapper.
|
|
54
155
|
#[wasm_bindgen(method, js_name=restore, catch)]
|
|
55
156
|
pub fn _restore(this: &JsPerspectiveViewerPlugin, token: &JsValue, columns_config: &JsValue) -> ApiResult<()>;
|
|
56
157
|
|
|
158
|
+
/// Free the plugin's resources. Serialized like the rendering methods —
|
|
159
|
+
/// callers route through the draw lock (`Renderer::dispose`/`delete`
|
|
160
|
+
/// defer teardown through it; a sync `delete` mid-`draw` violates the
|
|
161
|
+
/// call discipline).
|
|
57
162
|
#[wasm_bindgen(method)]
|
|
58
163
|
pub fn delete(this: &JsPerspectiveViewerPlugin);
|
|
59
164
|
|
|
60
|
-
#[wasm_bindgen(method)]
|
|
61
|
-
|
|
165
|
+
#[wasm_bindgen(method, js_name = restyle)]
|
|
166
|
+
fn _restyle(
|
|
62
167
|
this: &JsPerspectiveViewerPlugin,
|
|
63
168
|
);
|
|
64
169
|
|
|
65
|
-
#[wasm_bindgen(method, catch)]
|
|
66
|
-
|
|
170
|
+
#[wasm_bindgen(method, catch, js_name = render)]
|
|
171
|
+
async fn _render(
|
|
67
172
|
this: &JsPerspectiveViewerPlugin,
|
|
68
173
|
view: perspective_js::View,
|
|
69
174
|
viewport: Option<JsViewWindow>,
|
|
70
175
|
) -> ApiResult<web_sys::Blob>;
|
|
71
176
|
|
|
177
|
+
/// Full render of a `View` that is NEW to this plugin — dispatched iff
|
|
178
|
+
/// `bind_view` REBUILT the engine `View`. The only REQUIRED rendering
|
|
179
|
+
/// method.
|
|
72
180
|
#[wasm_bindgen(method, catch)]
|
|
73
181
|
pub async fn draw(
|
|
74
182
|
this: &JsPerspectiveViewerPlugin,
|
|
@@ -78,8 +186,8 @@ extern "C" {
|
|
|
78
186
|
force: bool
|
|
79
187
|
) -> ApiResult<()>;
|
|
80
188
|
|
|
81
|
-
#[wasm_bindgen(method, catch)]
|
|
82
|
-
|
|
189
|
+
#[wasm_bindgen(method, catch, js_name = update)]
|
|
190
|
+
async fn _update(
|
|
83
191
|
this: &JsPerspectiveViewerPlugin,
|
|
84
192
|
view: perspective_js::View,
|
|
85
193
|
column_limit: Option<usize>,
|
|
@@ -87,12 +195,17 @@ extern "C" {
|
|
|
87
195
|
force: bool
|
|
88
196
|
) -> ApiResult<()>;
|
|
89
197
|
|
|
90
|
-
#[wasm_bindgen(method, catch)]
|
|
91
|
-
|
|
198
|
+
#[wasm_bindgen(method, catch, js_name = clear)]
|
|
199
|
+
async fn _clear(this: &JsPerspectiveViewerPlugin) -> ApiResult<JsValue>;
|
|
92
200
|
|
|
93
|
-
#[wasm_bindgen(method, catch)]
|
|
94
|
-
|
|
201
|
+
#[wasm_bindgen(method, catch, js_name = resize)]
|
|
202
|
+
async fn _resize(this: &JsPerspectiveViewerPlugin) -> ApiResult<JsValue>;
|
|
203
|
+
|
|
204
|
+
#[wasm_bindgen(method, catch, js_name = deselect)]
|
|
205
|
+
async fn _deselect(this: &JsPerspectiveViewerPlugin) -> ApiResult<()>;
|
|
95
206
|
|
|
207
|
+
#[wasm_bindgen(method, catch, js_name = presize)]
|
|
208
|
+
async fn _presize(this: &JsPerspectiveViewerPlugin, width: f64, height: f64) -> ApiResult<JsValue>;
|
|
96
209
|
}
|
|
97
210
|
|
|
98
211
|
impl From<JsPluginStaticConfig> for PluginStaticConfig {
|
|
@@ -101,6 +214,49 @@ impl From<JsPluginStaticConfig> for PluginStaticConfig {
|
|
|
101
214
|
}
|
|
102
215
|
}
|
|
103
216
|
|
|
217
|
+
/// Which OPTIONAL plugin methods a plugin implements (see "Capability
|
|
218
|
+
/// tiers" on [`JsPerspectiveViewerPlugin`]). Detected once per
|
|
219
|
+
/// custom-element tag — methods live on the class prototype, so every
|
|
220
|
+
/// instance of a tag answers identically — and memoized; call sites read
|
|
221
|
+
/// cached flags instead of running per-call `Reflect` probes.
|
|
222
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
223
|
+
pub struct PluginCapabilities {
|
|
224
|
+
pub draw: bool,
|
|
225
|
+
pub update: bool,
|
|
226
|
+
pub resize: bool,
|
|
227
|
+
pub restyle: bool,
|
|
228
|
+
pub clear: bool,
|
|
229
|
+
pub deselect: bool,
|
|
230
|
+
pub presize: bool,
|
|
231
|
+
pub render: bool,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
thread_local! {
|
|
235
|
+
static PLUGIN_CAPABILITIES: RefCell<HashMap<String, PluginCapabilities>> =
|
|
236
|
+
RefCell::new(HashMap::new());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
impl PluginCapabilities {
|
|
240
|
+
fn detect(plugin: &JsPerspectiveViewerPlugin) -> Self {
|
|
241
|
+
let is_fn = |name: &str| {
|
|
242
|
+
js_sys::Reflect::get(plugin, &JsValue::from_str(name))
|
|
243
|
+
.map(|x| x.is_function())
|
|
244
|
+
.unwrap_or_default()
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
Self {
|
|
248
|
+
draw: is_fn("draw"),
|
|
249
|
+
update: is_fn("update"),
|
|
250
|
+
resize: is_fn("resize"),
|
|
251
|
+
restyle: is_fn("restyle"),
|
|
252
|
+
clear: is_fn("clear"),
|
|
253
|
+
deselect: is_fn("deselect"),
|
|
254
|
+
presize: is_fn("presize"),
|
|
255
|
+
render: is_fn("render"),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
104
260
|
impl JsPerspectiveViewerPlugin {
|
|
105
261
|
/// Read and deserialize the plugin's static config. Should only
|
|
106
262
|
/// be called once per plugin (at registration time); cache the
|
|
@@ -118,4 +274,135 @@ impl JsPerspectiveViewerPlugin {
|
|
|
118
274
|
let columns_config = JsValue::from_serde_ext(&columns_config).unwrap();
|
|
119
275
|
self._restore(token, &columns_config)
|
|
120
276
|
}
|
|
277
|
+
|
|
278
|
+
/// This plugin's memoized [`PluginCapabilities`] — the first call for a
|
|
279
|
+
/// given element tag runs the `Reflect` probes; all later calls (any
|
|
280
|
+
/// instance of the tag) read the cache.
|
|
281
|
+
pub fn capabilities(&self) -> PluginCapabilities {
|
|
282
|
+
let tag = self.unchecked_ref::<web_sys::Element>().tag_name();
|
|
283
|
+
PLUGIN_CAPABILITIES.with(|cache| {
|
|
284
|
+
*cache
|
|
285
|
+
.borrow_mut()
|
|
286
|
+
.entry(tag)
|
|
287
|
+
.or_insert_with(|| PluginCapabilities::detect(self))
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Repaint of the SAME `View` — dispatched iff one of the six
|
|
292
|
+
/// plugin-visible sources changed (see "Dispatch semantics" on
|
|
293
|
+
/// [`JsPerspectiveViewerPlugin`]); never defensively. Reaches the
|
|
294
|
+
/// plugin via [`crate::renderer::Renderer::update_bound`] (pipeline
|
|
295
|
+
/// runs — `Adopted` deltas, changed-config delivery, public no-op
|
|
296
|
+
/// refresh) or [`crate::renderer::Renderer::update_lazy`]
|
|
297
|
+
/// (`table_updated` data refreshes, the config-apply tasks, warning
|
|
298
|
+
/// dismiss), both locked. Tier fallback: a plugin without `update`
|
|
299
|
+
/// receives `draw` — it declared no incremental path, so every repaint
|
|
300
|
+
/// is a full render.
|
|
301
|
+
pub async fn update(
|
|
302
|
+
&self,
|
|
303
|
+
view: perspective_js::View,
|
|
304
|
+
column_limit: Option<usize>,
|
|
305
|
+
row_limit: Option<usize>,
|
|
306
|
+
force: bool,
|
|
307
|
+
) -> ApiResult<()> {
|
|
308
|
+
if self.capabilities().update {
|
|
309
|
+
self._update(view, column_limit, row_limit, force).await
|
|
310
|
+
} else {
|
|
311
|
+
self.draw(view, column_limit, row_limit, force).await
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Repaint from retained state — dispatched iff geometry or visibility
|
|
316
|
+
/// changed: box resizes (resize observers, presize sweeps, settings
|
|
317
|
+
/// toggles) and the panel-ACTIVATION chrome nudge
|
|
318
|
+
/// ([`crate::renderer::Renderer::activation_repaint`], stamped inside
|
|
319
|
+
/// one locked dispatch). Same `View`, same data, no CSS re-read —
|
|
320
|
+
/// implementations should no-op while hidden (`offsetParent == null`).
|
|
321
|
+
/// Tier fallback: no-op — CSS reflows the box and the next dispatch
|
|
322
|
+
/// repaints the content.
|
|
323
|
+
pub async fn resize(&self) -> ApiResult<JsValue> {
|
|
324
|
+
if self.capabilities().resize {
|
|
325
|
+
self._resize().await
|
|
326
|
+
} else {
|
|
327
|
+
Ok(JsValue::UNDEFINED)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Re-read the `--psp-*` CSS custom properties (sync). Dispatched ONLY
|
|
332
|
+
/// when the effective theme genuinely changed, state-keyed by
|
|
333
|
+
/// [`crate::renderer::Renderer::needs_restyle`] (the effective theme
|
|
334
|
+
/// vs. the one recorded at this plugin's last capture — first paint or
|
|
335
|
+
/// last restyle), from two sites: fused immediately BEFORE a
|
|
336
|
+
/// `draw`/`update` inside the same locked dispatch
|
|
337
|
+
/// (`Renderer::draw_view`, "restyle then draw" — one render pass in
|
|
338
|
+
/// the new theme), or as `Renderer::restyle_all`'s locked
|
|
339
|
+
/// restyle-then-`update` pair when no other dispatch is coming
|
|
340
|
+
/// (theme picker, `resetThemes`, default-theme discovery, restore
|
|
341
|
+
/// tails). Exception: the public `restyleElement()` API restyles
|
|
342
|
+
/// unconditionally — it is the "my external CSS changed" affordance,
|
|
343
|
+
/// outside what captured-theme state can know. (Plus one
|
|
344
|
+
/// `mount_plugin` restyle on first light-DOM mount.) Tier fallback:
|
|
345
|
+
/// no-op — the plugin re-reads CSS at its next render.
|
|
346
|
+
pub fn restyle(&self) {
|
|
347
|
+
if self.capabilities().restyle {
|
|
348
|
+
self._restyle()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Blank the plugin (view deleted, table removed). Tier fallback:
|
|
353
|
+
/// no-op.
|
|
354
|
+
pub async fn clear(&self) -> ApiResult<JsValue> {
|
|
355
|
+
if self.capabilities().clear {
|
|
356
|
+
self._clear().await
|
|
357
|
+
} else {
|
|
358
|
+
Ok(JsValue::UNDEFINED)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// Offscreen export render (`copy`/`export`) — returns an image
|
|
363
|
+
/// `Blob`; not a dispatch verb (does not touch the plugin's mounted
|
|
364
|
+
/// DOM state), but still serialized under the draw lock. Tier
|
|
365
|
+
/// fallback: errors — image export is unsupported without it.
|
|
366
|
+
pub async fn render(
|
|
367
|
+
&self,
|
|
368
|
+
view: perspective_js::View,
|
|
369
|
+
viewport: Option<JsViewWindow>,
|
|
370
|
+
) -> ApiResult<web_sys::Blob> {
|
|
371
|
+
if self.capabilities().render {
|
|
372
|
+
self._render(view, viewport).await
|
|
373
|
+
} else {
|
|
374
|
+
Err(ApiError::from("Plugin does not support image export"))
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// Clear any visible selection state (highlighted rows, pinned
|
|
379
|
+
/// tooltips) WITHOUT emitting selection events. Invoked by the host
|
|
380
|
+
/// when an element-level global filter contributed by this panel's
|
|
381
|
+
/// selection is removed (`GlobalFilterBar` chip × / "Clear"), so the
|
|
382
|
+
/// selection visual can't outlive the filter it produced.
|
|
383
|
+
/// Implementations may redraw, so callers must hold the plugin's
|
|
384
|
+
/// per-`Renderer` draw lock (see "Call discipline"). Tier fallback:
|
|
385
|
+
/// no-op.
|
|
386
|
+
pub async fn deselect(&self) -> ApiResult<()> {
|
|
387
|
+
if self.capabilities().deselect {
|
|
388
|
+
self._deselect().await
|
|
389
|
+
} else {
|
|
390
|
+
Ok(())
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// Staged presize: render at the TARGET element box `(width, height)`
|
|
395
|
+
/// offscreen — nothing on screen changes — resolving, once the frame
|
|
396
|
+
/// is staged, to a present closure that reveals it synchronously.
|
|
397
|
+
/// Callers run the closure in the same task as the layout commit the
|
|
398
|
+
/// presize anticipated (see `Renderer::presize_with_dimensions`), so
|
|
399
|
+
/// geometry and pixels land in one paint; the closure is lock-free (it
|
|
400
|
+
/// blits retained pixels, no plugin render entry). Only call when
|
|
401
|
+
/// [`Self::capabilities`] reports `presize` — plugins without it take
|
|
402
|
+
/// the held style-override path. Resolves `None` when the plugin
|
|
403
|
+
/// skipped (hidden), presenting nothing.
|
|
404
|
+
pub async fn presize(&self, width: f64, height: f64) -> ApiResult<Option<js_sys::Function>> {
|
|
405
|
+
let present = self._presize(width, height).await?;
|
|
406
|
+
Ok(present.dyn_into::<js_sys::Function>().ok())
|
|
407
|
+
}
|
|
121
408
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
//! A `wasm-bindgen` binding to the `<regular-layout>` custom element from
|
|
14
|
+
//! [`regular-layout`](https://github.com/texodus/regular-layout), used as the
|
|
15
|
+
//! layout engine for `<perspective-viewer>`'s panels.
|
|
16
|
+
|
|
17
|
+
use js_sys::Promise;
|
|
18
|
+
use serde::{Deserialize, Serialize};
|
|
19
|
+
use wasm_bindgen::prelude::*;
|
|
20
|
+
|
|
21
|
+
#[wasm_bindgen]
|
|
22
|
+
extern "C" {
|
|
23
|
+
/// The `<regular-layout>` custom element.
|
|
24
|
+
#[wasm_bindgen(extends = web_sys::HtmlElement)]
|
|
25
|
+
pub type RegularLayout;
|
|
26
|
+
|
|
27
|
+
/// Place the child named `name` into the grid, optionally at `path`
|
|
28
|
+
/// (a `number[]`; pass [`JsValue::UNDEFINED`] to append) and optionally
|
|
29
|
+
/// forcing a `split` (`bool` or an `Orientation` string).
|
|
30
|
+
#[wasm_bindgen(method, js_name = "insertPanel")]
|
|
31
|
+
pub fn insert_panel(this: &RegularLayout, name: &str, path: JsValue, split: JsValue)
|
|
32
|
+
-> Promise;
|
|
33
|
+
|
|
34
|
+
/// Remove the panel named `name` from the grid (its DOM child stays
|
|
35
|
+
/// connected, but unslotted).
|
|
36
|
+
#[wasm_bindgen(method, js_name = "removePanel")]
|
|
37
|
+
pub fn remove_panel(this: &RegularLayout, name: &str) -> Promise;
|
|
38
|
+
|
|
39
|
+
/// Unslot all panels.
|
|
40
|
+
#[wasm_bindgen(method)]
|
|
41
|
+
pub fn clear(this: &RegularLayout) -> Promise;
|
|
42
|
+
|
|
43
|
+
/// Return the `number[]` index path for the panel named `name`, or `null`.
|
|
44
|
+
#[wasm_bindgen(method, js_name = "calculatePath")]
|
|
45
|
+
pub fn calculate_path(this: &RegularLayout, name: &str) -> JsValue;
|
|
46
|
+
|
|
47
|
+
/// Hit-test for the [`LayoutPath`](https://github.com/texodus/regular-layout)
|
|
48
|
+
/// under a pointer (`{clientX, clientY}`), or `null`.
|
|
49
|
+
#[wasm_bindgen(method, js_name = "calculateIntersect")]
|
|
50
|
+
pub fn calculate_intersect(this: &RegularLayout, coordinates: &JsValue) -> JsValue;
|
|
51
|
+
|
|
52
|
+
/// Show the drag-preview overlay for `target` at the pointer `coordinates`.
|
|
53
|
+
#[wasm_bindgen(method, js_name = "setOverlayState")]
|
|
54
|
+
pub fn set_overlay_state(
|
|
55
|
+
this: &RegularLayout,
|
|
56
|
+
coordinates: &JsValue,
|
|
57
|
+
target: &JsValue,
|
|
58
|
+
) -> Promise;
|
|
59
|
+
|
|
60
|
+
/// Clear the drag-preview overlay and commit the placement for `target`.
|
|
61
|
+
#[wasm_bindgen(method, js_name = "clearOverlayState")]
|
|
62
|
+
pub fn clear_overlay_state(
|
|
63
|
+
this: &RegularLayout,
|
|
64
|
+
coordinates: &JsValue,
|
|
65
|
+
target: &JsValue,
|
|
66
|
+
) -> Promise;
|
|
67
|
+
|
|
68
|
+
/// Euclidean pixel distance between `coordinates` and a drag `target`.
|
|
69
|
+
#[wasm_bindgen(method, js_name = "diffCoordinates")]
|
|
70
|
+
pub fn diff_coordinates(this: &RegularLayout, coordinates: &JsValue, target: &JsValue) -> f64;
|
|
71
|
+
|
|
72
|
+
/// Convert a `ViewWindow` (normalized 0–1) to a `DOMRect` in screen pixels.
|
|
73
|
+
#[wasm_bindgen(method, js_name = "realCoordinates")]
|
|
74
|
+
pub fn real_coordinates(this: &RegularLayout, window: &JsValue) -> JsValue;
|
|
75
|
+
|
|
76
|
+
/// Configure resize physics (edge tolerances, divider size).
|
|
77
|
+
#[wasm_bindgen(method, js_name = "restorePhysics")]
|
|
78
|
+
pub fn restore_physics(this: &RegularLayout, physics: &JsValue);
|
|
79
|
+
|
|
80
|
+
/// Read the current resize physics.
|
|
81
|
+
#[wasm_bindgen(method, js_name = "savePhysics")]
|
|
82
|
+
pub fn save_physics(this: &RegularLayout) -> JsValue;
|
|
83
|
+
|
|
84
|
+
/// Resume a resize previously deferred via `preventDefault()` in the
|
|
85
|
+
/// `regular-layout-before-resize` handler.
|
|
86
|
+
#[wasm_bindgen(method, js_name = "resumeResize")]
|
|
87
|
+
pub fn resume_resize(this: &RegularLayout);
|
|
88
|
+
|
|
89
|
+
/// Serialize the current [`Layout`] tree.
|
|
90
|
+
#[wasm_bindgen(method)]
|
|
91
|
+
pub fn save(this: &RegularLayout) -> JsValue;
|
|
92
|
+
|
|
93
|
+
/// Restore a [`Layout`] tree, dispatching a cancelable
|
|
94
|
+
/// `regular-layout-before-resize` event first.
|
|
95
|
+
#[wasm_bindgen(method)]
|
|
96
|
+
pub fn restore(this: &RegularLayout, layout: &JsValue) -> Promise;
|
|
97
|
+
|
|
98
|
+
/// Restore a [`Layout`] tree synchronously, without the resize event.
|
|
99
|
+
#[wasm_bindgen(method, js_name = "restoreSync")]
|
|
100
|
+
pub fn restore_sync(this: &RegularLayout, layout: &JsValue);
|
|
101
|
+
|
|
102
|
+
/// Select the panel `name`, making it the front-most tab within its
|
|
103
|
+
/// containing stack; dispatches a `regular-layout-select` event
|
|
104
|
+
/// (`detail: { name }`). Fires even when re-selecting the active tab.
|
|
105
|
+
#[wasm_bindgen(method)]
|
|
106
|
+
pub fn select(this: &RegularLayout, name: &str) -> Promise;
|
|
107
|
+
|
|
108
|
+
/// Maximize the panel `name` (full-size, others hidden). Transient — not
|
|
109
|
+
/// persisted by [`Self::save`]; a subsequent [`Self::restore`] resets it.
|
|
110
|
+
#[wasm_bindgen(method)]
|
|
111
|
+
pub fn maximize(this: &RegularLayout, name: &str);
|
|
112
|
+
|
|
113
|
+
/// Restore the normal multi-panel view after [`Self::maximize`].
|
|
114
|
+
#[wasm_bindgen(method)]
|
|
115
|
+
pub fn minimize(this: &RegularLayout);
|
|
116
|
+
|
|
117
|
+
/// The `detail` of a [`Self::BEFORE_RESIZE_EVENT`] event.
|
|
118
|
+
pub type PresizeDetail;
|
|
119
|
+
|
|
120
|
+
/// Each panel's pending-resize target as a `Record<name, LayoutPath>`; each
|
|
121
|
+
/// `LayoutPath` carries a normalized `view_window` that
|
|
122
|
+
/// [`Self::real_coordinates`] converts to a pixel box. Used to pre-size
|
|
123
|
+
/// panel contents before the layout commits.
|
|
124
|
+
#[wasm_bindgen(method, js_name = "calculatePresizePaths")]
|
|
125
|
+
pub fn calculate_presize_paths(this: &PresizeDetail) -> JsValue;
|
|
126
|
+
|
|
127
|
+
/// For a drag transition, the dragged panel's `{slot, path}` — its name
|
|
128
|
+
/// and the `LayoutPath` of its preview box (`path.view_window` is exactly
|
|
129
|
+
/// where the overlay renders it). The dragged panel is removed from the
|
|
130
|
+
/// layout tree during a drag, so it is absent from
|
|
131
|
+
/// [`Self::calculate_presize_paths`]; this getter lets consumers pre-size
|
|
132
|
+
/// it like any other panel (regular-layout >= 0.6.1). `null` when the
|
|
133
|
+
/// pointer is outside every drop target (the dragged panel is hidden),
|
|
134
|
+
/// `undefined` for non-drag transitions.
|
|
135
|
+
#[wasm_bindgen(method, getter)]
|
|
136
|
+
pub fn overlay(this: &PresizeDetail) -> JsValue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl RegularLayout {
|
|
140
|
+
/// Emitted before the layout resizes its panels, so consumers can pre-size
|
|
141
|
+
/// panel contents (`CustomEvent<PresizeDetail>`); cancelable.
|
|
142
|
+
pub const BEFORE_RESIZE_EVENT: &'static str = "regular-layout-before-resize";
|
|
143
|
+
/// Emitted before the layout tree changes (`CustomEvent<Layout>`).
|
|
144
|
+
pub const BEFORE_UPDATE_EVENT: &'static str = "regular-layout-before-update";
|
|
145
|
+
/// The `<regular-layout-frame>` chrome element tag name (titlebar + tabs).
|
|
146
|
+
pub const FRAME_TAG_NAME: &'static str = "regular-layout-frame";
|
|
147
|
+
/// Emitted when a tab is selected (`CustomEvent<{ name: string }>`). Fires
|
|
148
|
+
/// on every tab interaction — including re-selecting the active tab or
|
|
149
|
+
/// selecting the sole tab of a single-element stack — so consumers can
|
|
150
|
+
/// always map a tab interaction to an "active panel".
|
|
151
|
+
pub const SELECT_EVENT: &'static str = "regular-layout-select";
|
|
152
|
+
/// The custom element tag name.
|
|
153
|
+
pub const TAG_NAME: &'static str = "regular-layout";
|
|
154
|
+
/// Emitted after the layout tree changes (`CustomEvent<Layout>`).
|
|
155
|
+
pub const UPDATE_EVENT: &'static str = "regular-layout-update";
|
|
156
|
+
|
|
157
|
+
/// Whether `name` is currently placed in this layout's tree (a non-null
|
|
158
|
+
/// [`Self::calculate_path`]).
|
|
159
|
+
pub fn contains_panel(&self, name: &str) -> bool {
|
|
160
|
+
let path = self.calculate_path(name);
|
|
161
|
+
!path.is_null() && !path.is_undefined()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// The orientation of a [`SplitLayout`].
|
|
166
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
|
|
167
|
+
#[serde(rename_all = "lowercase")]
|
|
168
|
+
pub enum Orientation {
|
|
169
|
+
Horizontal,
|
|
170
|
+
Vertical,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// A serde mirror of `regular-layout`'s serialized layout tree, as returned by
|
|
174
|
+
/// [`RegularLayout::save`] and accepted by [`RegularLayout::restore`].
|
|
175
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
|
176
|
+
#[serde(tag = "type")]
|
|
177
|
+
pub enum Layout {
|
|
178
|
+
/// Space divided among children, side-by-side or stacked.
|
|
179
|
+
#[serde(rename = "split-layout")]
|
|
180
|
+
Split(SplitLayout),
|
|
181
|
+
|
|
182
|
+
/// A leaf node holding one or more named tabs (a stack).
|
|
183
|
+
#[serde(rename = "tab-layout")]
|
|
184
|
+
Tab(TabLayout),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
|
188
|
+
pub struct SplitLayout {
|
|
189
|
+
pub children: Vec<Layout>,
|
|
190
|
+
pub sizes: Vec<f64>,
|
|
191
|
+
pub orientation: Orientation,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
|
|
195
|
+
pub struct TabLayout {
|
|
196
|
+
/// Panel ids (slot names) in this stack.
|
|
197
|
+
pub tabs: Vec<String>,
|
|
198
|
+
|
|
199
|
+
/// Index of the selected tab within `tabs`.
|
|
200
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
201
|
+
#[ts(optional)]
|
|
202
|
+
pub selected: Option<usize>,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl Layout {
|
|
206
|
+
/// Collect every panel id (slot name) in the tree, depth-first.
|
|
207
|
+
pub fn slot_names(&self) -> Vec<String> {
|
|
208
|
+
match self {
|
|
209
|
+
Layout::Split(split) => split.children.iter().flat_map(Layout::slot_names).collect(),
|
|
210
|
+
Layout::Tab(tab) => tab.tabs.clone(),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Collect every panel id that is *hidden* — i.e. in a stack (tab-layout)
|
|
215
|
+
/// at an index other than its `selected` one — in depth-first order. The
|
|
216
|
+
/// complement of the visible set: every other panel is the front of its
|
|
217
|
+
/// stack (single-tab leaves are trivially visible).
|
|
218
|
+
pub fn hidden_slot_names(&self) -> Vec<String> {
|
|
219
|
+
match self {
|
|
220
|
+
Layout::Split(split) => split
|
|
221
|
+
.children
|
|
222
|
+
.iter()
|
|
223
|
+
.flat_map(Layout::hidden_slot_names)
|
|
224
|
+
.collect(),
|
|
225
|
+
Layout::Tab(tab) => {
|
|
226
|
+
let selected = tab.selected.unwrap_or(0);
|
|
227
|
+
tab.tabs
|
|
228
|
+
.iter()
|
|
229
|
+
.enumerate()
|
|
230
|
+
.filter(|(idx, _)| *idx != selected)
|
|
231
|
+
.map(|(_, name)| name.clone())
|
|
232
|
+
.collect()
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Rewrite every panel id through `f`, returning the remapped tree. Ids for
|
|
238
|
+
/// which `f` returns `None` are left unchanged. Used by whole-element
|
|
239
|
+
/// `restore`, which recreates panels under fresh (collision-free) ids and
|
|
240
|
+
/// must point the saved layout tree at them.
|
|
241
|
+
pub fn remap(&self, f: &impl Fn(&str) -> Option<String>) -> Layout {
|
|
242
|
+
match self {
|
|
243
|
+
Layout::Split(split) => Layout::Split(SplitLayout {
|
|
244
|
+
children: split.children.iter().map(|c| c.remap(f)).collect(),
|
|
245
|
+
sizes: split.sizes.clone(),
|
|
246
|
+
orientation: split.orientation,
|
|
247
|
+
}),
|
|
248
|
+
Layout::Tab(tab) => Layout::Tab(TabLayout {
|
|
249
|
+
tabs: tab
|
|
250
|
+
.tabs
|
|
251
|
+
.iter()
|
|
252
|
+
.map(|name| f(name).unwrap_or_else(|| name.clone()))
|
|
253
|
+
.collect(),
|
|
254
|
+
selected: tab.selected,
|
|
255
|
+
}),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|