@perspective-dev/viewer 5.5.1 → 5.6.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/README.md +82 -3
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -1
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/ar.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 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.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/css/velvet.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.js +2 -0
- package/dist/esm/column-format.js.map +7 -0
- package/dist/esm/extensions.d.ts +3 -13
- package/dist/esm/perspective-viewer.d.ts +1 -1
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +4 -1
- package/dist/esm/select-detail.d.ts +15 -0
- package/dist/esm/select-detail.js +2 -0
- package/dist/esm/select-detail.js.map +7 -0
- package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
- package/dist/esm/ts-rs/TableRef.d.ts +9 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
- package/dist/wasm/perspective-viewer.d.ts +115 -35
- package/dist/wasm/perspective-viewer.js +314 -190
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +31 -18
- package/src/css/column-selector.css +205 -50
- package/src/css/ui/containers/context-menu.css +12 -1
- package/src/css/ui/containers/dropdown-menu-items.css +29 -0
- package/src/css/ui/containers/scroll-panel.css +4 -1
- package/src/rust/agent/client/protocol.rs +0 -89
- package/src/rust/agent/markdown/mod.rs +0 -238
- package/src/rust/agent/preamble.md +1 -0
- package/src/rust/agent/tools.rs +2 -16
- package/src/rust/components/column_selector/active_column.rs +15 -5
- package/src/rust/components/column_selector/add_expression_button.rs +9 -26
- package/src/rust/components/column_selector/config_selector.rs +34 -6
- package/src/rust/components/column_selector/derived_control.rs +229 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
- package/src/rust/components/column_selector/filter_column.rs +0 -2
- package/src/rust/components/column_selector/inactive_column.rs +4 -5
- package/src/rust/components/column_selector/pivot_column.rs +21 -1
- package/src/rust/components/column_selector/sort_column.rs +4 -3
- package/src/rust/components/column_selector.rs +21 -1
- package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
- package/src/rust/components/column_settings_sidebar.rs +25 -19
- package/src/rust/components/debug_panel.rs +38 -29
- package/src/rust/components/dragdrop_list.rs +8 -9
- package/src/rust/components/main_panel/msg.rs +6 -4
- package/src/rust/components/main_panel/reconcile.rs +6 -1
- package/src/rust/components/main_panel/render.rs +70 -51
- package/src/rust/components/main_panel/update.rs +39 -1
- package/src/rust/components/main_panel.rs +97 -59
- package/src/rust/components/new_panel_menu.rs +69 -1
- package/src/rust/components/panel_menu.rs +63 -12
- package/src/rust/components/plugin_tab.rs +3 -2
- package/src/rust/components/render_warning.rs +2 -1
- package/src/rust/components/settings_panel.rs +4 -4
- package/src/rust/components/status_bar.rs +74 -24
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +13 -1
- package/src/rust/components/viewer/msg.rs +4 -0
- package/src/rust/components/viewer/panels.rs +41 -1
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/settings.rs +0 -22
- package/src/rust/components/viewer/snapshots.rs +2 -2
- package/src/rust/components/viewer/wiring.rs +14 -0
- package/src/rust/components/viewer.rs +45 -25
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- package/src/rust/config/number_string_format.rs +0 -94
- package/src/rust/config/options.rs +3 -7
- package/src/rust/config/plugin_static_config.rs +0 -42
- package/src/rust/config/viewer_config.rs +47 -17
- package/src/rust/config/workspace_config.rs +158 -128
- package/src/rust/custom_elements/viewer.rs +187 -131
- package/src/rust/custom_events.rs +33 -18
- package/src/rust/derived.rs +266 -0
- package/src/rust/exprtk/tokenize.rs +0 -54
- package/src/rust/js/plugin.rs +1 -1
- package/src/rust/lib.rs +3 -0
- package/src/rust/presentation.rs +23 -4
- package/src/rust/queries/column_locator.rs +55 -48
- package/src/rust/queries/get_viewer_config.rs +1 -1
- package/src/rust/queries/plugin_column_styles.rs +22 -23
- package/src/rust/renderer/dispatch.rs +2 -0
- package/src/rust/renderer/plugin_config.rs +652 -487
- package/src/rust/renderer/props.rs +4 -0
- package/src/rust/renderer/registry.rs +2 -6
- package/src/rust/renderer/state.rs +205 -0
- package/src/rust/renderer.rs +201 -59
- package/src/rust/session/metadata.rs +30 -9
- package/src/rust/session/op_queue.rs +430 -0
- package/src/rust/session/panel_state.rs +327 -0
- package/src/rust/session/view_subscription.rs +42 -3
- package/src/rust/session.rs +900 -395
- package/src/rust/tasks/apply_global_filters.rs +49 -35
- package/src/rust/tasks/auto_pause.rs +22 -12
- package/src/rust/tasks/copy_export.rs +5 -0
- package/src/rust/tasks/create_panel.rs +34 -9
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +145 -89
- package/src/rust/tasks/reset_all.rs +31 -20
- package/src/rust/tasks/restore_derived.rs +200 -0
- package/src/rust/tasks/restore_panel.rs +153 -141
- package/src/rust/tasks/send_column_config.rs +56 -29
- package/src/rust/tasks/send_plugin_config.rs +52 -23
- package/src/rust/tasks/set_edit_mode.rs +33 -25
- package/src/rust/tasks/sync_update_panels.rs +39 -12
- package/src/rust/tasks/table_lifecycle.rs +70 -34
- package/src/rust/tasks/transactional_restore.rs +387 -0
- package/src/rust/tasks/update_theme.rs +45 -6
- package/src/rust/ui/containers/scroll_panel.rs +17 -10
- package/src/rust/ui/containers/split_panel.rs +0 -41
- package/src/rust/ui/context_menu.rs +157 -18
- package/src/rust/ui/font_loader.rs +23 -35
- package/src/rust/ui/form/mirrored_textarea.rs +0 -12
- package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
- package/src/rust/ui/form/palette_selector.rs +0 -51
- package/src/rust/ui/portal.rs +4 -10
- package/src/rust/utils/browser/mod.rs +2 -3
- package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
- package/src/rust/utils/browser/request_animation_frame.rs +27 -8
- package/src/rust/utils/css.rs +0 -360
- package/src/rust/utils/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +2 -3
- package/src/rust/utils/modal_position.rs +15 -21
- package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
- package/src/rust/utils/pubsub.rs +0 -1
- package/src/rust/workspace.rs +187 -140
- package/src/svg/derived.svg +6 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +10 -0
- package/src/themes/intl/de.css +10 -0
- package/src/themes/intl/es.css +10 -0
- package/src/themes/intl/fr.css +10 -0
- package/src/themes/intl/ja.css +10 -0
- package/src/themes/intl/pt.css +10 -0
- package/src/themes/intl/zh.css +10 -0
- package/src/themes/intl.css +10 -0
- package/src/ts/extensions.ts +3 -36
- package/src/ts/perspective-viewer.ts +1 -1
- package/src/ts/plugin.ts +4 -1
- package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
- package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
- package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
- package/src/ts/ts-rs/RestoreOptions.ts +3 -7
- package/src/ts/ts-rs/TableRef.ts +9 -0
- package/src/ts/ts-rs/ViewerConfig.ts +7 -6
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
- package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
- package/dist/esm/perspective-viewer.inline.d.ts +0 -3
- package/dist/esm/perspective-viewer.inline.js +0 -21
- package/dist/esm/perspective-viewer.inline.js.map +0 -7
- package/src/rust/tasks/restore_and_render.rs +0 -107
- package/src/rust/utils/browser/tests/debounce.rs +0 -163
- package/src/rust/utils/tests/mod.rs +0 -14
- package/src/rust/utils/tests/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
|
@@ -24,7 +24,7 @@ use crate::components::column_selector::{EmptyColumn, InPlaceColumn, InvalidColu
|
|
|
24
24
|
use crate::components::type_icon::TypeIcon;
|
|
25
25
|
use crate::presentation::{DragDropContainer, Presentation};
|
|
26
26
|
use crate::ui::intl_slug;
|
|
27
|
-
use crate::utils::DragTarget;
|
|
27
|
+
use crate::utils::{DragTarget, left_click};
|
|
28
28
|
|
|
29
29
|
#[derive(Properties, Derivative)]
|
|
30
30
|
#[derivative(Clone(bound = ""))]
|
|
@@ -161,8 +161,6 @@ where
|
|
|
161
161
|
if freeze && self.frozen_size.is_none() {
|
|
162
162
|
let elem = self.elem.cast::<HtmlElement>().unwrap();
|
|
163
163
|
self.frozen_size = Some({
|
|
164
|
-
// `offset_width` and family are `i32`, but Chrome _really_
|
|
165
|
-
// uses fractional pixel widths for these which can only be
|
|
166
164
|
// recovered by parsing the generated stylesheet ...
|
|
167
165
|
let txt = window()
|
|
168
166
|
.unwrap()
|
|
@@ -178,7 +176,8 @@ where
|
|
|
178
176
|
});
|
|
179
177
|
true
|
|
180
178
|
} else if !freeze {
|
|
181
|
-
// Don't render because the invoker will do so through
|
|
179
|
+
// Don't render because the invoker will do so through
|
|
180
|
+
// `dragdrop`.
|
|
182
181
|
self.frozen_size = None;
|
|
183
182
|
false
|
|
184
183
|
} else {
|
|
@@ -191,8 +190,8 @@ where
|
|
|
191
190
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
192
191
|
let dragover = Callback::from(|_event: DragEvent| _event.prevent_default());
|
|
193
192
|
|
|
194
|
-
// On dragleave, signal the parent but no need to redraw as parent will
|
|
195
|
-
// `change()` when resetting props.
|
|
193
|
+
// On dragleave, signal the parent but no need to redraw as parent will
|
|
194
|
+
// call `change()` when resetting props.
|
|
196
195
|
let drag_container = DragDropContainer::new(
|
|
197
196
|
{
|
|
198
197
|
let total = ctx.props().children.len();
|
|
@@ -244,7 +243,7 @@ where
|
|
|
244
243
|
.map(|x| V::is_self_move(x))
|
|
245
244
|
.unwrap_or_default();
|
|
246
245
|
|
|
247
|
-
let close = ctx.props().parent.callback(|_| V::close(0));
|
|
246
|
+
let close = left_click(ctx.props().parent.callback(|_| V::close(0)));
|
|
248
247
|
let dragenter = ctx.props().parent.callback({
|
|
249
248
|
let container_noderef = container_noderef.clone();
|
|
250
249
|
move |event: DragEvent| {
|
|
@@ -332,7 +331,7 @@ where
|
|
|
332
331
|
.into_iter()
|
|
333
332
|
.enumerate()
|
|
334
333
|
.map(|(idx, column)| {
|
|
335
|
-
let close = ctx.props().parent.callback(move |_| V::close(idx));
|
|
334
|
+
let close = left_click(ctx.props().parent.callback(move |_| V::close(idx)));
|
|
336
335
|
let dragenter = ctx.props().parent.callback({
|
|
337
336
|
let link = ctx.link().clone();
|
|
338
337
|
let container_noderef = container_noderef.clone();
|
|
@@ -441,7 +440,7 @@ where
|
|
|
441
440
|
</div>
|
|
442
441
|
<span
|
|
443
442
|
class="toggle-mode is_column_active"
|
|
444
|
-
onmousedown={ctx.props().parent.callback(move |_| V::close(0))}
|
|
443
|
+
onmousedown={left_click(ctx.props().parent.callback(move |_| V::close(0)))}
|
|
445
444
|
/>
|
|
446
445
|
</div>
|
|
447
446
|
} else if show_empty {
|
|
@@ -49,12 +49,14 @@ pub enum MainPanelMsg {
|
|
|
49
49
|
BeforeResize(web_sys::Event),
|
|
50
50
|
|
|
51
51
|
/// A right-click opened the panel context menu (target panel id, client
|
|
52
|
-
/// x, y). Fired by the imperative `contextmenu` listener on the
|
|
53
|
-
///
|
|
52
|
+
/// x, y). Fired by the imperative `contextmenu` listener on the viewer
|
|
53
|
+
/// host — see `_contextmenu_listener` — and by each `PanelTab`.
|
|
54
54
|
/// `Some(id)` activates that panel and opens its
|
|
55
55
|
/// [`PanelMenu`](crate::components::panel_menu::PanelMenu) at the
|
|
56
|
-
/// cursor; `None`
|
|
57
|
-
///
|
|
56
|
+
/// cursor; `None` (no frame on the composed path — status bar,
|
|
57
|
+
/// sidebars, the empty stage) resolves to the ACTIVE panel, else to a
|
|
58
|
+
/// target-less menu offering only "New", whose items create the first
|
|
59
|
+
/// panel.
|
|
58
60
|
ContextMenu(Option<String>, f64, f64),
|
|
59
61
|
|
|
60
62
|
/// Dismiss the panel context menu (the menu session ended).
|
|
@@ -111,6 +111,8 @@ impl MainPanel {
|
|
|
111
111
|
if self.listener_target.as_ref() != Some(&el) {
|
|
112
112
|
if self.listener_target.is_some() {
|
|
113
113
|
self.inserted.clear();
|
|
114
|
+
self.pending_insertions.clear();
|
|
115
|
+
self.suspect_closes.clear();
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
let _ = el.add_event_listener_with_callback(
|
|
@@ -183,11 +185,13 @@ impl MainPanel {
|
|
|
183
185
|
// horizontal) rather than the default (path `[]`), which would
|
|
184
186
|
// *stack* into the root tab-layout — a stack only renders its
|
|
185
187
|
// selected tab, and without frame chrome there's no tab-bar to
|
|
186
|
-
// reach the others. Splitting keeps every panel visible
|
|
188
|
+
// reach the others. Splitting keeps every panel visible
|
|
189
|
+
// side-by-side.
|
|
187
190
|
let index = self.inserted.len();
|
|
188
191
|
let path = JsValue::from(js_sys::Array::of1(&JsValue::from_f64(index as f64)));
|
|
189
192
|
let _ = layout.insert_panel(name, path, JsValue::from_bool(true));
|
|
190
193
|
self.inserted.push(name.to_owned());
|
|
194
|
+
self.pending_insertions.insert(name.to_owned());
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
let mut pending = std::mem::take(&mut self.pending_removals);
|
|
@@ -197,6 +201,7 @@ impl MainPanel {
|
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
if pending.insert(name.clone()) {
|
|
204
|
+
self.pending_insertions.remove(name);
|
|
200
205
|
let _ = layout.remove_panel(name);
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -58,19 +58,21 @@ impl MainPanel {
|
|
|
58
58
|
let on_dismiss_warning = dismiss_render_warning_callback(session, renderer);
|
|
59
59
|
|
|
60
60
|
// Each panel's plugin is mounted in the viewer's light DOM under its
|
|
61
|
-
// panel-id slot (see `renderer::activate`). Render one
|
|
62
|
-
// cell per panel: a concrete `<div name=…>`
|
|
63
|
-
// `regular-layout` grid-places via its
|
|
64
|
-
// only matches *direct*,
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
61
|
+
// panel-id slot (see `renderer::activate`). Render one
|
|
62
|
+
// `<regular-layout>` cell per panel: a concrete `<div name=…>`
|
|
63
|
+
// wrapper (which `regular-layout` grid-places via its
|
|
64
|
+
// `::slotted([name])` rule — that only matches *direct*,
|
|
65
|
+
// non-`<slot>` children, hence the wrapper) whose
|
|
66
|
+
// inner forwarding `<slot>` projects the light-DOM plugin into the
|
|
67
|
+
// placed cell. The `<regular-layout>` element is ALWAYS
|
|
68
|
+
// rendered — at zero panels it is merely empty (its tree
|
|
69
|
+
// collapses to a childless split, and it paints nothing of its
|
|
70
|
+
// own), which keeps the stage CSS (viewer.css keys the stage
|
|
71
|
+
// background + inset bleed on the element) and the attached
|
|
72
|
+
// listeners alive across empty↔populated transitions.
|
|
71
73
|
let plugin_area = {
|
|
72
|
-
// The active panel (whose engines the status bar/settings target)
|
|
73
|
-
// the one the active renderer is bound to.
|
|
74
|
+
// The active panel (whose engines the status bar/settings target)
|
|
75
|
+
// is the one the active renderer is bound to.
|
|
74
76
|
let active_slot = renderer.slot_name();
|
|
75
77
|
// Panel-count chrome (`single`/`multi`, closable, draggable)
|
|
76
78
|
let placed_count = ctx.props().workspace.placed_count();
|
|
@@ -99,20 +101,25 @@ impl MainPanel {
|
|
|
99
101
|
class.push("active");
|
|
100
102
|
}
|
|
101
103
|
} else {
|
|
102
|
-
// Lone panel: can't be closed to zero — hide the close
|
|
103
|
-
// (see viewer.css) and
|
|
104
|
+
// Lone panel: can't be closed to zero — hide the close
|
|
105
|
+
// button (see viewer.css) and
|
|
106
|
+
// skip the active outline.
|
|
104
107
|
class.push("single");
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
// Activate this panel on pointerdown anywhere in it (body
|
|
108
|
-
// titlebar), so *working* in a panel
|
|
109
|
-
// —
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
110
|
+
// Activate this panel on pointerdown anywhere in it (body
|
|
111
|
+
// or titlebar), so *working* in a panel
|
|
112
|
+
// — not only clicking its tab — targets
|
|
113
|
+
// it with the shared settings/toolbar. regular-layout
|
|
114
|
+
// emits `regular-layout-select` on tab clicks only, which
|
|
115
|
+
// left body interaction unable to
|
|
116
|
+
// change the active panel. Tab clicks
|
|
117
|
+
// still also fire `-select`, but that dispatch is async
|
|
118
|
+
// (after `restore`) so it lands after
|
|
119
|
+
// this synchronous handler — within a
|
|
120
|
+
// stack, switching tabs still resolves to the clicked
|
|
121
|
+
// panel. Re-activating the
|
|
122
|
+
// already-active panel is a no-op upstream.
|
|
116
123
|
let on_activate = ctx.props().on_activate_panel.clone();
|
|
117
124
|
let activate_name = name.clone();
|
|
118
125
|
let onpointerdown = Callback::from(move |_: web_sys::PointerEvent| {
|
|
@@ -121,25 +128,33 @@ impl MainPanel {
|
|
|
121
128
|
}
|
|
122
129
|
});
|
|
123
130
|
|
|
124
|
-
// Right-click anywhere in the panel opens the context menu.
|
|
125
|
-
// is handled by the imperative
|
|
131
|
+
// Right-click anywhere in the panel opens the context menu.
|
|
132
|
+
// This is handled by the imperative
|
|
133
|
+
// `contextmenu` listener on the
|
|
126
134
|
// panel container (see `_contextmenu_listener`), NOT a Yew
|
|
127
|
-
// `oncontextmenu` here: the plugin body is light-DOM
|
|
128
|
-
// the renderer, so Yew's
|
|
129
|
-
//
|
|
135
|
+
// `oncontextmenu` here: the plugin body is light-DOM
|
|
136
|
+
// attached by the renderer, so Yew's
|
|
137
|
+
// delegated handler — which walks the vdom
|
|
138
|
+
// — never matches a right-click on it, leaving the native
|
|
139
|
+
// menu.
|
|
130
140
|
|
|
131
141
|
// `<regular-layout-frame>` gives each panel a titlebar (its
|
|
132
|
-
// `slot="tab"`) and a `::part(container)`. Two forwarding
|
|
133
|
-
// project our light-DOM nodes
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
142
|
+
// `slot="tab"`) and a `::part(container)`. Two forwarding
|
|
143
|
+
// `<slot>`s project our light-DOM nodes
|
|
144
|
+
// in: `name` → the plugin (into the
|
|
145
|
+
// container), and `tab-<name>` (with `slot="tab"`) → this
|
|
146
|
+
// panel's `<PanelTab>` (into the
|
|
147
|
+
// titlebar). Both targets live in the
|
|
148
|
+
// viewer's light DOM, so the slots — being in the viewer's
|
|
149
|
+
// shadow — collect them and forward
|
|
150
|
+
// them one level deeper into the frame.
|
|
138
151
|
//
|
|
139
|
-
// The frame no longer carries the theme: the per-panel
|
|
140
|
-
// applied directly to the
|
|
141
|
-
//
|
|
142
|
-
//
|
|
152
|
+
// The frame no longer carries the theme: the per-panel
|
|
153
|
+
// theme is applied directly to the
|
|
154
|
+
// light-DOM plugin + tab (each stamped with
|
|
155
|
+
// a `theme` attribute) via the `perspective-viewer
|
|
156
|
+
// [theme="X"]` document rules, so it
|
|
157
|
+
// themes through the native cascade instead
|
|
143
158
|
// of a scraped/inlined `--psp-*` block.
|
|
144
159
|
html! {
|
|
145
160
|
<regular-layout-frame
|
|
@@ -154,10 +169,11 @@ impl MainPanel {
|
|
|
154
169
|
}
|
|
155
170
|
});
|
|
156
171
|
|
|
157
|
-
// Per-panel tab titles: regular-layout's tabs render their label
|
|
158
|
-
// a `--regular-layout-<id>--title` custom property
|
|
159
|
-
// set one per titled panel on the
|
|
160
|
-
// inherit into each
|
|
172
|
+
// Per-panel tab titles: regular-layout's tabs render their label
|
|
173
|
+
// from a `--regular-layout-<id>--title` custom property
|
|
174
|
+
// (a CSS string). We set one per titled panel on the
|
|
175
|
+
// `<regular-layout>` host; they inherit into each
|
|
176
|
+
// frame/tab's shadow.
|
|
161
177
|
let title_style = ctx
|
|
162
178
|
.props()
|
|
163
179
|
.panel_titles
|
|
@@ -171,10 +187,11 @@ impl MainPanel {
|
|
|
171
187
|
|
|
172
188
|
// One `<PanelTab>` per panel, mounted in the viewer's light DOM and
|
|
173
189
|
// forwarded into its frame's titlebar by the `tab-<id>` slot above.
|
|
174
|
-
// Rendered as siblings of `<regular-layout>` (their DOM placement
|
|
175
|
-
// irrelevant — each portals itself onto the viewer
|
|
176
|
-
// drives regular-layout's own
|
|
177
|
-
// listener picks
|
|
190
|
+
// Rendered as siblings of `<regular-layout>` (their DOM placement
|
|
191
|
+
// is irrelevant — each portals itself onto the viewer
|
|
192
|
+
// host). `on_select` drives regular-layout's own
|
|
193
|
+
// `select` (the `regular-layout-select` listener picks
|
|
194
|
+
// up activation).
|
|
178
195
|
let viewer_elem = ctx.props().presentation.viewer_elem().clone();
|
|
179
196
|
// A lone panel can't be closed to zero, and has nowhere to be
|
|
180
197
|
// dragged — so it's neither closable nor draggable; its tab also
|
|
@@ -199,12 +216,13 @@ impl MainPanel {
|
|
|
199
216
|
// `regular-layout` on the re-render. Driving the layout first — and
|
|
200
217
|
// discovering the close via the `regular-layout-update` DOM event —
|
|
201
218
|
// ran the layout-update resize fan-out over a still-present-but-
|
|
202
|
-
// doomed panel, spawning an (uncancellable) draw that then raced
|
|
203
|
-
// disposal ("restore() called before load()").
|
|
219
|
+
// doomed panel, spawning an (uncancellable) draw that then raced
|
|
220
|
+
// its disposal ("restore() called before load()").
|
|
204
221
|
let on_close = ctx.props().on_close_panel.clone();
|
|
205
|
-
// Commit an in-tab title edit to that panel's own session — the
|
|
206
|
-
// `title_changed` → `TitlesChanged` refresh loop
|
|
207
|
-
// driven by the status bar re-renders
|
|
222
|
+
// Commit an in-tab title edit to that panel's own session — the
|
|
223
|
+
// same `title_changed` → `TitlesChanged` refresh loop
|
|
224
|
+
// that used to be driven by the status bar re-renders
|
|
225
|
+
// the tab with the new title.
|
|
208
226
|
let on_rename = {
|
|
209
227
|
let workspace = ctx.props().workspace.clone();
|
|
210
228
|
Callback::from(move |(id, title): (String, Option<String>)| {
|
|
@@ -406,6 +424,7 @@ impl MainPanel {
|
|
|
406
424
|
on_reset={ctx.props().on_reset.clone()}
|
|
407
425
|
{on_new_panel}
|
|
408
426
|
session_props={ctx.props().session_props.clone()}
|
|
427
|
+
render_error={ctx.props().renderer_props.render_error.clone()}
|
|
409
428
|
presentation_props={ctx.props().presentation_props.clone()}
|
|
410
429
|
is_settings_open={ctx.props().is_settings_open}
|
|
411
430
|
update_count={ctx.props().update_count}
|
|
@@ -19,7 +19,8 @@ use perspective_js::utils::JsValueSerdeExt;
|
|
|
19
19
|
use wasm_bindgen::prelude::*;
|
|
20
20
|
use yew::prelude::*;
|
|
21
21
|
|
|
22
|
-
use super::MainPanel;
|
|
22
|
+
use super::{MainPanel, MainPanelMsg};
|
|
23
|
+
use crate::ApiFuture;
|
|
23
24
|
use crate::components::panel_menu::PanelCommand;
|
|
24
25
|
use crate::js::{Layout, RegularLayout};
|
|
25
26
|
use crate::renderer::SUBPIXEL_EPSILON;
|
|
@@ -58,15 +59,40 @@ impl MainPanel {
|
|
|
58
59
|
|
|
59
60
|
let layout: RegularLayout = el.unchecked_into();
|
|
60
61
|
let mut closed = Vec::new();
|
|
62
|
+
let mut pending_insertions = std::mem::take(&mut self.pending_insertions);
|
|
63
|
+
let mut suspect_closes = std::mem::take(&mut self.suspect_closes);
|
|
61
64
|
self.inserted.retain(|id| {
|
|
62
65
|
if layout.contains_panel(id) {
|
|
66
|
+
pending_insertions.remove(id);
|
|
67
|
+
suspect_closes.remove(id);
|
|
68
|
+
true
|
|
69
|
+
} else if pending_insertions.contains(id) {
|
|
70
|
+
// Issued to the layout but not yet in a committed tree —
|
|
71
|
+
// the pre-commit window, not a close.
|
|
72
|
+
true
|
|
73
|
+
} else if ctx.props().panel_ids.iter().any(|p| p.as_str() == id)
|
|
74
|
+
&& suspect_closes.insert(id.clone())
|
|
75
|
+
{
|
|
63
76
|
true
|
|
64
77
|
} else {
|
|
78
|
+
suspect_closes.remove(id);
|
|
65
79
|
closed.push(id.clone());
|
|
66
80
|
false
|
|
67
81
|
}
|
|
68
82
|
});
|
|
69
83
|
|
|
84
|
+
self.pending_insertions = pending_insertions;
|
|
85
|
+
let recheck = !suspect_closes.is_empty();
|
|
86
|
+
self.suspect_closes = suspect_closes;
|
|
87
|
+
if recheck {
|
|
88
|
+
let link = ctx.link().clone();
|
|
89
|
+
ApiFuture::spawn(async move {
|
|
90
|
+
crate::utils::request_animation_frame().await;
|
|
91
|
+
link.send_message(MainPanelMsg::LayoutUpdated);
|
|
92
|
+
Ok(())
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
let closed_any = !closed.is_empty();
|
|
71
97
|
for id in closed {
|
|
72
98
|
self.pending_removals.remove(&id);
|
|
@@ -160,6 +186,18 @@ impl MainPanel {
|
|
|
160
186
|
x: f64,
|
|
161
187
|
y: f64,
|
|
162
188
|
) -> bool {
|
|
189
|
+
// A frame-less right-click (status bar, sidebars) targets the
|
|
190
|
+
// ACTIVE panel, validated against the workspace so a stale slot
|
|
191
|
+
// name after an eject falls back to the target-less stage menu.
|
|
192
|
+
let id = id.or_else(|| {
|
|
193
|
+
ctx.props().renderer.slot_name().filter(|name| {
|
|
194
|
+
ctx.props()
|
|
195
|
+
.workspace
|
|
196
|
+
.panel(&crate::workspace::PanelId::from(name.as_str()))
|
|
197
|
+
.is_some()
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
|
|
163
201
|
// Make the right-clicked panel active so active-targeting
|
|
164
202
|
// commands (e.g. Reset) act on it; then show the menu.
|
|
165
203
|
if let Some(id) = &id {
|
|
@@ -185,14 +185,15 @@ pub struct MainPanel {
|
|
|
185
185
|
/// and attached alongside the others.
|
|
186
186
|
_layout_before_resize_listener: Closure<dyn FnMut(web_sys::Event)>,
|
|
187
187
|
|
|
188
|
-
/// `contextmenu` listener on the
|
|
189
|
-
/// point covering
|
|
190
|
-
///
|
|
191
|
-
///
|
|
192
|
-
///
|
|
193
|
-
/// is
|
|
194
|
-
///
|
|
195
|
-
///
|
|
188
|
+
/// `contextmenu` listener on the viewer HOST element — one stable attach
|
|
189
|
+
/// point covering every surface (panels, status bar, sidebars),
|
|
190
|
+
/// independent of the layout reconcile; attached once in `rendered` and
|
|
191
|
+
/// REMOVED in `destroy`, since the host outlives this component's Yew
|
|
192
|
+
/// app. Imperative — not a Yew `oncontextmenu` — because the plugin body
|
|
193
|
+
/// is light-DOM attached by the renderer (its DOM parent is the host,
|
|
194
|
+
/// not the frame), so Yew's delegated handler never matches a
|
|
195
|
+
/// right-click there; a native listener sees every surface — shadow
|
|
196
|
+
/// chrome and slotted light DOM alike — on the composed path.
|
|
196
197
|
_contextmenu_listener: Closure<dyn FnMut(web_sys::Event)>,
|
|
197
198
|
|
|
198
199
|
/// The `<regular-layout>` ELEMENT the layout listeners are attached to.
|
|
@@ -211,6 +212,20 @@ pub struct MainPanel {
|
|
|
211
212
|
/// layout and not yet committed.
|
|
212
213
|
pending_removals: HashSet<String>,
|
|
213
214
|
|
|
215
|
+
/// Ids whose `insertPanel` has been issued to the layout and not yet
|
|
216
|
+
/// observed in a committed tree: layout commits are async, so a
|
|
217
|
+
/// multi-insert reconcile sees `regular-layout-update` events BETWEEN
|
|
218
|
+
/// commits, and close detection must not mistake the pre-commit window
|
|
219
|
+
/// for a user close (it once disposed the second of two restored
|
|
220
|
+
/// panels mid-restore).
|
|
221
|
+
pending_insertions: HashSet<String>,
|
|
222
|
+
|
|
223
|
+
/// MODEL-held panels observed absent from exactly one committed tree,
|
|
224
|
+
/// awaiting the next `regular-layout-update` (rescheduled one frame out)
|
|
225
|
+
/// to confirm a layout-originated close — an unrelated removal's commit
|
|
226
|
+
/// can report a live sibling absent transiently.
|
|
227
|
+
suspect_closes: HashSet<String>,
|
|
228
|
+
|
|
214
229
|
/// Per-panel `ResizeObserver`s, keyed by panel id, each observing that
|
|
215
230
|
/// panel's slotted plugin element and resizing only that panel's
|
|
216
231
|
/// `Renderer`. Bound in `BeforeResize` for the DRAGGED panel only (it is
|
|
@@ -226,9 +241,9 @@ pub struct MainPanel {
|
|
|
226
241
|
hidden_tabs: HashSet<String>,
|
|
227
242
|
|
|
228
243
|
/// Open context menu as `(client x, client y, target panel id)`; outer
|
|
229
|
-
/// `None` when closed. A `None` *panel id* is the
|
|
230
|
-
///
|
|
231
|
-
/// [`PanelMenu`](super::panel_menu::PanelMenu) overlay.
|
|
244
|
+
/// `None` when closed. A `None` *panel id* is the target-less menu (no
|
|
245
|
+
/// active panel to resolve to — "New" only). Rendered as a
|
|
246
|
+
/// cursor-anchored [`PanelMenu`](super::panel_menu::PanelMenu) overlay.
|
|
232
247
|
context_menu: Option<(f64, f64, Option<String>)>,
|
|
233
248
|
|
|
234
249
|
/// Id of the currently maximized panel (via `regular-layout.maximize`), or
|
|
@@ -280,27 +295,29 @@ impl Component for MainPanel {
|
|
|
280
295
|
}) as Box<dyn FnMut(web_sys::Event)>);
|
|
281
296
|
|
|
282
297
|
// `preventDefault()` synchronously suspends the layout's resize commit
|
|
283
|
-
// (the event is cancelable); the component then pre-sizes each panel
|
|
284
|
-
// calls `resumeResize` to release it (see
|
|
298
|
+
// (the event is cancelable); the component then pre-sizes each panel
|
|
299
|
+
// and calls `resumeResize` to release it (see
|
|
300
|
+
// `MainPanelMsg::BeforeResize`).
|
|
285
301
|
let before_resize_cb = ctx.link().callback(MainPanelMsg::BeforeResize);
|
|
286
302
|
let before_resize_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
287
303
|
event.prevent_default();
|
|
288
304
|
before_resize_cb.emit(event);
|
|
289
305
|
}) as Box<dyn FnMut(web_sys::Event)>);
|
|
290
306
|
|
|
291
|
-
// Imperative `contextmenu` listener: a right-click anywhere in
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
307
|
+
// Imperative `contextmenu` listener: a right-click anywhere in the
|
|
308
|
+
// element opens the panel menu — targeting the frame on the event's
|
|
309
|
+
// composed path, or `None` (resolved to the ACTIVE panel, else the
|
|
310
|
+
// stage menu, in `on_context_menu`, where props live) from any
|
|
311
|
+
// other surface. Two pass-throughs keep the NATIVE menu: editable
|
|
312
|
+
// targets (inputs, textareas, contenteditable — cut/copy/paste),
|
|
313
|
+
// and anything inside an open `PortalModal` (every modal host is a
|
|
314
|
+
// light-DOM viewer child in the modal slot — this menu included, so
|
|
315
|
+
// without the carve-out a right-click in any open dropdown would
|
|
316
|
+
// re-open the menu underneath it).
|
|
301
317
|
let contextmenu_cb = ctx
|
|
302
318
|
.link()
|
|
303
319
|
.callback(|(id, x, y)| MainPanelMsg::ContextMenu(id, x, y));
|
|
320
|
+
let viewer_elem = ctx.props().presentation.viewer_elem().clone();
|
|
304
321
|
let contextmenu_listener = Closure::wrap(Box::new(move |event: web_sys::Event| {
|
|
305
322
|
// Shift+right-click passes through to the native browser menu.
|
|
306
323
|
if event.unchecked_ref::<web_sys::MouseEvent>().shift_key() {
|
|
@@ -308,11 +325,34 @@ impl Component for MainPanel {
|
|
|
308
325
|
}
|
|
309
326
|
|
|
310
327
|
let path = event.composed_path();
|
|
328
|
+
let is_editable = path.get(0).dyn_ref::<web_sys::HtmlElement>().is_some_and(
|
|
329
|
+
|el: &web_sys::HtmlElement| {
|
|
330
|
+
el.tag_name().eq_ignore_ascii_case("input")
|
|
331
|
+
|| el.tag_name().eq_ignore_ascii_case("textarea")
|
|
332
|
+
|| el.is_content_editable()
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if is_editable {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
311
340
|
let mut panel_id = None;
|
|
312
341
|
for i in 0..path.length() {
|
|
313
342
|
let node = path.get(i);
|
|
314
|
-
|
|
315
|
-
|
|
343
|
+
let Some(el) = node.dyn_ref::<web_sys::Element>() else {
|
|
344
|
+
continue;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if el == viewer_elem.unchecked_ref::<web_sys::Element>() {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if el.slot() == crate::ui::MODAL_SLOT {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if el.tag_name().eq_ignore_ascii_case("regular-layout-frame")
|
|
316
356
|
&& let Some(name) = el.get_attribute("name")
|
|
317
357
|
{
|
|
318
358
|
panel_id = Some(name);
|
|
@@ -320,29 +360,9 @@ impl Component for MainPanel {
|
|
|
320
360
|
}
|
|
321
361
|
}
|
|
322
362
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
.and_then(|t| t.dyn_into::<web_sys::Element>().ok())
|
|
327
|
-
.is_some_and(|el| {
|
|
328
|
-
el.query_selector("regular-layout-frame")
|
|
329
|
-
.ok()
|
|
330
|
-
.flatten()
|
|
331
|
-
.is_none()
|
|
332
|
-
})
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
if let Some(id) = panel_id {
|
|
336
|
-
event.prevent_default();
|
|
337
|
-
let mouse = event.unchecked_ref::<web_sys::MouseEvent>();
|
|
338
|
-
contextmenu_cb.emit((Some(id), mouse.client_x() as f64, mouse.client_y() as f64));
|
|
339
|
-
} else if is_empty_stage() {
|
|
340
|
-
event.prevent_default();
|
|
341
|
-
let mouse = event.unchecked_ref::<web_sys::MouseEvent>();
|
|
342
|
-
contextmenu_cb.emit((None, mouse.client_x() as f64, mouse.client_y() as f64));
|
|
343
|
-
}
|
|
344
|
-
// With panels present, a click outside every frame lets the
|
|
345
|
-
// native menu through.
|
|
363
|
+
event.prevent_default();
|
|
364
|
+
let mouse = event.unchecked_ref::<web_sys::MouseEvent>();
|
|
365
|
+
contextmenu_cb.emit((panel_id, mouse.client_x() as f64, mouse.client_y() as f64));
|
|
346
366
|
}) as Box<dyn FnMut(web_sys::Event)>);
|
|
347
367
|
|
|
348
368
|
let staged_sub = {
|
|
@@ -368,6 +388,8 @@ impl Component for MainPanel {
|
|
|
368
388
|
layout_ref: NodeRef::default(),
|
|
369
389
|
inserted: Vec::new(),
|
|
370
390
|
pending_removals: HashSet::new(),
|
|
391
|
+
pending_insertions: HashSet::new(),
|
|
392
|
+
suspect_closes: HashSet::new(),
|
|
371
393
|
_layout_update_listener: listener,
|
|
372
394
|
_layout_select_listener: select_listener,
|
|
373
395
|
_layout_before_resize_listener: before_resize_listener,
|
|
@@ -404,16 +426,20 @@ impl Component for MainPanel {
|
|
|
404
426
|
}
|
|
405
427
|
|
|
406
428
|
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
|
|
407
|
-
// The `contextmenu` listener attaches to the
|
|
408
|
-
// stable attach point covering
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// menu.
|
|
412
|
-
if first_render
|
|
413
|
-
let _ =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
429
|
+
// The `contextmenu` listener attaches to the viewer HOST element —
|
|
430
|
+
// one stable attach point covering every surface (panels, status
|
|
431
|
+
// bar, sidebars), decoupled from the layout reconcile. Panel
|
|
432
|
+
// right-clicks reach it through their frames on the composed path;
|
|
433
|
+
// everything else resolves to the active panel (or the stage menu).
|
|
434
|
+
if first_render {
|
|
435
|
+
let _ = ctx
|
|
436
|
+
.props()
|
|
437
|
+
.presentation
|
|
438
|
+
.viewer_elem()
|
|
439
|
+
.add_event_listener_with_callback(
|
|
440
|
+
"contextmenu",
|
|
441
|
+
self._contextmenu_listener.as_ref().unchecked_ref(),
|
|
442
|
+
);
|
|
417
443
|
}
|
|
418
444
|
|
|
419
445
|
self.size_staging_wrappers();
|
|
@@ -425,7 +451,19 @@ impl Component for MainPanel {
|
|
|
425
451
|
self.render(ctx)
|
|
426
452
|
}
|
|
427
453
|
|
|
428
|
-
fn destroy(&mut self,
|
|
454
|
+
fn destroy(&mut self, ctx: &Context<Self>) {
|
|
455
|
+
// The host element outlives this component (`delete()` tears down
|
|
456
|
+
// the Yew app, not the custom element), so the listener must not
|
|
457
|
+
// dangle into a dropped `Closure`.
|
|
458
|
+
let _ = ctx
|
|
459
|
+
.props()
|
|
460
|
+
.presentation
|
|
461
|
+
.viewer_elem()
|
|
462
|
+
.remove_event_listener_with_callback(
|
|
463
|
+
"contextmenu",
|
|
464
|
+
self._contextmenu_listener.as_ref().unchecked_ref(),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
429
467
|
}
|
|
430
468
|
|
|
431
469
|
impl MainPanel {
|