@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.
Files changed (178) hide show
  1. package/dist/cdn/perspective-viewer.js +20 -1
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/botanical.css +1 -1
  4. package/dist/css/dracula.css +1 -1
  5. package/dist/css/gruvbox-dark.css +1 -1
  6. package/dist/css/gruvbox.css +1 -1
  7. package/dist/css/icons.css +1 -1
  8. package/dist/css/monokai.css +1 -1
  9. package/dist/css/phosphor.css +1 -1
  10. package/dist/css/pro-dark.css +1 -1
  11. package/dist/css/pro.css +1 -1
  12. package/dist/css/solarized-dark.css +1 -1
  13. package/dist/css/solarized.css +1 -1
  14. package/dist/css/themes.css +1 -1
  15. package/dist/css/vaporwave.css +1 -1
  16. package/dist/esm/extensions.d.ts +22 -3
  17. package/dist/esm/perspective-viewer.d.ts +9 -0
  18. package/dist/esm/perspective-viewer.inline.js +20 -1
  19. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  20. package/dist/esm/perspective-viewer.js +20 -1
  21. package/dist/esm/perspective-viewer.js.map +4 -4
  22. package/dist/esm/plugin.d.ts +71 -5
  23. package/dist/esm/ts-rs/ClientOptions.d.ts +7 -0
  24. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -0
  25. package/dist/esm/ts-rs/ExportOptions.d.ts +9 -0
  26. package/dist/esm/ts-rs/GetClientOptions.d.ts +8 -0
  27. package/dist/esm/ts-rs/GetTableOptions.d.ts +8 -0
  28. package/dist/esm/ts-rs/Layout.d.ts +11 -0
  29. package/dist/esm/ts-rs/Orientation.d.ts +4 -0
  30. package/dist/esm/ts-rs/PanelOptions.d.ts +7 -0
  31. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +39 -0
  32. package/dist/esm/ts-rs/SplitLayout.d.ts +7 -0
  33. package/dist/esm/ts-rs/TabLayout.d.ts +10 -0
  34. package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -2
  35. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +40 -0
  36. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +28 -0
  37. package/dist/wasm/perspective-viewer.d.ts +211 -77
  38. package/dist/wasm/perspective-viewer.js +528 -180
  39. package/dist/wasm/perspective-viewer.wasm +0 -0
  40. package/dist/wasm/perspective-viewer.wasm.d.ts +34 -26
  41. package/dist/wasm/snippets/perspective-viewer-daeef4ceb066e6c7/inline0.js +6 -0
  42. package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline3.js → perspective-viewer-daeef4ceb066e6c7/inline4.js} +5 -0
  43. package/package.json +3 -2
  44. package/src/css/_context_menu.css +14 -0
  45. package/src/css/column-settings-panel.css +1 -1
  46. package/src/css/containers/context-menu.css +101 -0
  47. package/src/css/panel-tab.css +193 -0
  48. package/src/css/plugin-selector.css +7 -10
  49. package/src/css/status-bar.css +75 -81
  50. package/src/css/viewer.css +180 -70
  51. package/src/rust/components/column_dropdown.rs +40 -30
  52. package/src/rust/components/column_selector/active_column.rs +7 -18
  53. package/src/rust/components/column_selector/aggregate_selector.rs +4 -7
  54. package/src/rust/components/column_selector/config_selector.rs +33 -81
  55. package/src/rust/components/column_selector/filter_column.rs +5 -15
  56. package/src/rust/components/column_selector/inactive_column.rs +3 -8
  57. package/src/rust/components/column_selector/sort_column.rs +3 -6
  58. package/src/rust/components/column_selector.rs +5 -11
  59. package/src/rust/components/containers/split_panel.rs +175 -62
  60. package/src/rust/components/context_menu.rs +130 -0
  61. package/src/rust/components/form/debug.rs +3 -0
  62. package/src/rust/components/global_filter_bar.rs +76 -0
  63. package/src/rust/components/main_panel/frame_theme.rs +186 -0
  64. package/src/rust/components/main_panel/msg.rs +62 -0
  65. package/src/rust/components/main_panel/presize.rs +220 -0
  66. package/src/rust/components/main_panel/reconcile.rs +224 -0
  67. package/src/rust/components/main_panel/render.rs +385 -0
  68. package/src/rust/components/main_panel/update.rs +184 -0
  69. package/src/rust/components/main_panel.rs +311 -71
  70. package/src/rust/components/mod.rs +4 -0
  71. package/src/rust/components/panel_menu.rs +486 -0
  72. package/src/rust/components/panel_tab.rs +505 -0
  73. package/src/rust/components/plugin_tab.rs +1 -1
  74. package/src/rust/components/settings_panel.rs +13 -5
  75. package/src/rust/components/status_bar.rs +110 -178
  76. package/src/rust/components/status_indicator.rs +6 -9
  77. package/src/rust/components/style/mod.rs +5 -0
  78. package/src/rust/components/style/style_provider.rs +2 -0
  79. package/src/rust/components/viewer/filters.rs +133 -0
  80. package/src/rust/components/viewer/msg.rs +180 -0
  81. package/src/rust/components/viewer/panels.rs +326 -0
  82. package/src/rust/components/viewer/render.rs +243 -0
  83. package/src/rust/components/viewer/settings.rs +402 -0
  84. package/src/rust/components/viewer/snapshots.rs +134 -0
  85. package/src/rust/components/viewer/wiring.rs +505 -0
  86. package/src/rust/components/viewer.rs +243 -756
  87. package/src/rust/config/export_method.rs +1 -1
  88. package/src/rust/config/mod.rs +4 -0
  89. package/src/rust/config/options.rs +71 -0
  90. package/src/rust/config/viewer_config.rs +24 -2
  91. package/src/rust/config/workspace_config.rs +92 -0
  92. package/src/rust/custom_elements/copy_dropdown.rs +3 -1
  93. package/src/rust/custom_elements/debug_plugin.rs +20 -1
  94. package/src/rust/custom_elements/export_dropdown.rs +3 -1
  95. package/src/rust/custom_elements/viewer.rs +1015 -286
  96. package/src/rust/custom_events.rs +212 -61
  97. package/src/rust/js/mod.rs +2 -0
  98. package/src/rust/js/plugin.rs +297 -10
  99. package/src/rust/js/regular_layout.rs +258 -0
  100. package/src/rust/lib.rs +20 -1
  101. package/src/rust/presentation.rs +66 -33
  102. package/src/rust/queries/get_viewer_config.rs +29 -11
  103. package/src/rust/queries/plugin_column_styles.rs +2 -2
  104. package/src/rust/renderer/activate.rs +41 -14
  105. package/src/rust/renderer/dispatch.rs +436 -0
  106. package/src/rust/renderer/plugin_config.rs +495 -0
  107. package/src/rust/renderer/registry.rs +18 -6
  108. package/src/rust/renderer/render_timer.rs +29 -10
  109. package/src/rust/renderer.rs +350 -837
  110. package/src/rust/session/column_defaults_update.rs +26 -8
  111. package/src/rust/session/view_subscription.rs +30 -15
  112. package/src/rust/session.rs +753 -264
  113. package/src/rust/tasks/apply_global_filters.rs +89 -0
  114. package/src/rust/tasks/auto_pause.rs +171 -0
  115. package/src/rust/tasks/copy_export.rs +29 -19
  116. package/src/rust/tasks/create_panel.rs +243 -0
  117. package/src/rust/tasks/dismiss_render_warning.rs +3 -1
  118. package/src/rust/tasks/edit_expression.rs +4 -4
  119. package/src/rust/tasks/eject.rs +64 -23
  120. package/src/rust/tasks/mod.rs +28 -4
  121. package/src/rust/tasks/pipeline.rs +411 -0
  122. package/src/rust/tasks/presize_panels.rs +324 -0
  123. package/src/rust/tasks/reset_all.rs +22 -12
  124. package/src/rust/tasks/resize_observer.rs +124 -87
  125. package/src/rust/tasks/restore_and_render.rs +80 -86
  126. package/src/rust/tasks/restore_panel.rs +145 -0
  127. package/src/rust/tasks/send_column_config.rs +5 -2
  128. package/src/rust/tasks/send_plugin_config.rs +5 -2
  129. package/src/rust/tasks/set_edit_mode.rs +59 -0
  130. package/src/rust/tasks/sync_update_panels.rs +152 -0
  131. package/src/rust/tasks/table_lifecycle.rs +158 -0
  132. package/src/rust/tasks/update_theme.rs +56 -12
  133. package/src/rust/utils/completion.rs +56 -0
  134. package/src/rust/utils/custom_element.rs +5 -0
  135. package/src/rust/utils/debounce.rs +130 -7
  136. package/src/rust/utils/mod.rs +6 -0
  137. package/src/rust/utils/pending_effects.rs +62 -0
  138. package/src/rust/utils/spawn.rs +33 -0
  139. package/src/rust/utils/weak_scope.rs +0 -10
  140. package/src/rust/workspace.rs +846 -0
  141. package/src/svg/active-indicator.svg +3 -0
  142. package/src/themes/botanical.css +5 -2
  143. package/src/themes/defaults.css +54 -2
  144. package/src/themes/dracula.css +6 -3
  145. package/src/themes/gruvbox-dark.css +5 -2
  146. package/src/themes/gruvbox.css +5 -2
  147. package/src/themes/icons.css +4 -2
  148. package/src/themes/monokai.css +5 -2
  149. package/src/themes/phosphor.css +5 -2
  150. package/src/themes/pro-dark.css +5 -2
  151. package/src/themes/pro.css +2 -1
  152. package/src/themes/solarized-dark.css +5 -2
  153. package/src/themes/solarized.css +5 -2
  154. package/src/themes/vaporwave.css +5 -2
  155. package/src/ts/bootstrap.ts +8 -7
  156. package/src/ts/extensions.ts +30 -2
  157. package/src/ts/perspective-viewer.ts +10 -0
  158. package/src/ts/plugin.ts +72 -5
  159. package/src/ts/ts-rs/ClientOptions.ts +7 -0
  160. package/src/ts/ts-rs/ExportMethod.ts +3 -0
  161. package/src/ts/ts-rs/ExportOptions.ts +8 -0
  162. package/src/ts/ts-rs/GetClientOptions.ts +7 -0
  163. package/src/ts/ts-rs/GetTableOptions.ts +7 -0
  164. package/src/ts/ts-rs/Layout.ts +9 -0
  165. package/src/ts/ts-rs/Orientation.ts +6 -0
  166. package/src/ts/ts-rs/PanelOptions.ts +7 -0
  167. package/src/ts/ts-rs/PanelViewerConfig.ts +15 -0
  168. package/src/ts/ts-rs/SplitLayout.ts +5 -0
  169. package/src/ts/ts-rs/TabLayout.ts +11 -0
  170. package/src/ts/ts-rs/ViewerConfig.ts +6 -2
  171. package/src/ts/ts-rs/WorkspaceConfig.ts +36 -0
  172. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +24 -0
  173. package/src/rust/tasks/intersection_observer.rs +0 -98
  174. package/src/rust/tasks/update_and_render.rs +0 -110
  175. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline0.js → perspective-viewer-daeef4ceb066e6c7/inline1.js} +0 -0
  176. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline1.js → perspective-viewer-daeef4ceb066e6c7/inline2.js} +0 -0
  177. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline2.js → perspective-viewer-daeef4ceb066e6c7/inline3.js} +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-93064d0d79855d97/inline4.js → perspective-viewer-daeef4ceb066e6c7/inline5.js} +0 -0
@@ -0,0 +1,186 @@
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
+ //! `MainPanel::stamp_frame_themes`: mirror each panel's effective theme
14
+ //! *background* onto its `<regular-layout-frame>`.
15
+ //!
16
+ //! The frames live in the viewer's shadow DOM, so the document theme rules
17
+ //! (`perspective-viewer [theme="X"]`) can never match them — the
18
+ //! `--psp--background-color` their `::part(container)` background resolves
19
+ //! (viewer.css) inherits down from the viewer host, i.e. always the *host*
20
+ //! theme. A frame is normally covered by its slotted plugin (which paints its
21
+ //! own per-panel themed background), but while the plugin is missing — not yet
22
+ //! mounted, first draw pending, or torn down — the host-theme container shows
23
+ //! through a panel themed differently, as a visible artifact.
24
+ //!
25
+ //! There is no native cascade channel from the light-DOM plugin to the frame
26
+ //! (siblings in the flattened tree), so the value is mirrored imperatively:
27
+ //! each panel's *plugin element* — light-DOM, slotted under the panel's slot,
28
+ //! mounted eagerly at panel creation (`create_panel_model` /
29
+ //! `Renderer::mount_active_plugin`) — is where the document theme rules
30
+ //! actually resolve, so its computed `--psp--background-color` is copied onto
31
+ //! the frame's inline style, where the frame's shadow parts inherit it. The
32
+ //! plugin is the ONLY sound source element:
33
+ //!
34
+ //! - An owned hidden probe child of the viewer does NOT work: an *unslotted*
35
+ //! light-DOM child of a shadow host is outside the flattened tree, and
36
+ //! `getComputedStyle` returns all-empty for it even though the document rules
37
+ //! match it syntactically (selector matching is DOM-tree; style computation
38
+ //! is flat-tree).
39
+ //! - The `<perspective-viewer-tab>` is slotted, but stamps its own `theme` attr
40
+ //! in `PanelTab::rendered` — an unordered sibling lifecycle relative to this
41
+ //! pass (the theme-stamp-lag class of bug).
42
+ //! - The plugin's `theme` attr, by contrast, is stamped *synchronously at the
43
+ //! mutation sites* (`update_theme`, `restorePanel`, …) before the events that
44
+ //! schedule this render, so it is current when this pass reads it.
45
+ //!
46
+ //! A read is trusted ONLY when the plugin's stamped attr equals the theme
47
+ //! being mirrored: a freshly-created panel's plugin is mounted *unstamped*
48
+ //! (the attr first lands inside its first locked dispatch), and reading it
49
+ //! early would silently mirror the HOST theme's values. Trusted reads seed
50
+ //! [`MainPanel::theme_backgrounds`], the fallback for frames with no readable
51
+ //! plugin (an unregistered plugin name stays lazily unmounted; plugin-switch
52
+ //! and teardown transients). A frame with neither leaves the pass gate
53
+ //! unlatched, so it retries on subsequent renders until the dispatch stamp
54
+ //! lands (the render that follows that dispatch's `update_count` bump).
55
+ //!
56
+ //! Unlike the plugin `theme`/`active` stamps (see the NOTE in [`reconcile`]),
57
+ //! the frame is pure viewer chrome — no plugin dispatch reads it — so
58
+ //! stamping it from this async `rendered` pass cannot split a plugin draw
59
+ //! across paints, and it needs no lock.
60
+
61
+ use wasm_bindgen::prelude::*;
62
+ use yew::prelude::*;
63
+
64
+ use super::MainPanel;
65
+ use crate::utils::PtrEqRc;
66
+ use crate::workspace::PanelId;
67
+
68
+ /// The custom property mirrored onto each frame: what viewer.css's
69
+ /// `.rl-panel::part(container)` background resolves.
70
+ const BACKGROUND_VAR: &str = "--psp--background-color";
71
+
72
+ /// The inputs the frame backgrounds were last computed from; re-mirroring is
73
+ /// skipped while these are unchanged, so the forced style recalcs of
74
+ /// `getComputedStyle` don't run on every render (e.g. the per-update
75
+ /// `update_count` renders of a streaming table). `available_themes` is part of
76
+ /// the key because theme CSS registering late changes the *computed* value
77
+ /// under an unchanged theme name.
78
+ pub(super) type FrameThemeSnapshot = (
79
+ Vec<PanelId>,
80
+ Vec<(String, Option<String>)>,
81
+ PtrEqRc<Vec<String>>,
82
+ );
83
+
84
+ impl MainPanel {
85
+ /// Mirror each panel's effective theme background (its own theme, else the
86
+ /// registry default — the same fallback the tab/menu use) onto its
87
+ /// `<regular-layout-frame>`'s inline `--psp--background-color`, read off
88
+ /// the panel's stamped plugin element (else the theme-keyed cache of prior
89
+ /// reads). A panel with no resolvable theme has the property removed,
90
+ /// falling back to the inherited host-theme value.
91
+ pub(super) fn stamp_frame_themes(&mut self, ctx: &Context<Self>) {
92
+ let Some(layout) = self.layout_ref.cast::<web_sys::Element>() else {
93
+ return;
94
+ };
95
+
96
+ let props = ctx.props();
97
+ let snapshot: FrameThemeSnapshot = (
98
+ props.panel_ids.clone(),
99
+ props.panel_themes.clone(),
100
+ props.presentation_props.available_themes.clone(),
101
+ );
102
+
103
+ if self.stamped_frame_themes.as_ref() == Some(&snapshot) {
104
+ return;
105
+ }
106
+
107
+ // A theme-registry change can re-value an unchanged theme name, so
108
+ // cached reads from the previous registry are unsound.
109
+ if let Some((_, _, prev_themes)) = &self.stamped_frame_themes
110
+ && prev_themes != &snapshot.2
111
+ {
112
+ self.theme_backgrounds.clear();
113
+ }
114
+
115
+ let viewer = props.presentation.viewer_elem().clone();
116
+ let mut complete = true;
117
+ let children = layout.children();
118
+ for i in 0..children.length() {
119
+ let Some(frame) = children.item(i) else {
120
+ continue;
121
+ };
122
+
123
+ if !frame
124
+ .tag_name()
125
+ .eq_ignore_ascii_case("regular-layout-frame")
126
+ {
127
+ continue;
128
+ }
129
+
130
+ let Some(name) = frame.get_attribute("name") else {
131
+ continue;
132
+ };
133
+
134
+ let theme = props.effective_panel_theme(&name);
135
+
136
+ let background = theme.as_ref().and_then(|theme| {
137
+ let fresh = read_plugin_background(&viewer, &name, theme);
138
+ if let Some(color) = &fresh {
139
+ self.theme_backgrounds.insert(theme.clone(), color.clone());
140
+ }
141
+
142
+ let value = fresh.or_else(|| self.theme_backgrounds.get(theme).cloned());
143
+ complete &= value.is_some();
144
+ value
145
+ });
146
+
147
+ let style = frame.unchecked_ref::<web_sys::HtmlElement>().style();
148
+ match background {
149
+ Some(color) => {
150
+ let _ = style.set_property(BACKGROUND_VAR, &color);
151
+ },
152
+ None => {
153
+ let _ = style.remove_property(BACKGROUND_VAR);
154
+ },
155
+ }
156
+ }
157
+
158
+ // Latch only a fully-resolved pass; an unresolved frame (unmounted or
159
+ // not-yet-stamped plugin) retries each render until its dispatch
160
+ // stamp lands.
161
+ self.stamped_frame_themes = complete.then_some(snapshot);
162
+ }
163
+ }
164
+
165
+ /// Read the computed [`BACKGROUND_VAR`] off `slot`'s plugin element — the
166
+ /// viewer light-DOM child mounted under exactly that slot name (the tab and
167
+ /// toolbar use `tab-`/`statusbar-extra-` prefixed slots). Requires the
168
+ /// plugin's `theme` attr to already equal `theme` — during a plugin switch
169
+ /// two elements briefly share the slot, and a freshly-mounted plugin is not
170
+ /// yet stamped; both are disambiguated by the attr check.
171
+ fn read_plugin_background(viewer: &web_sys::Element, slot: &str, theme: &str) -> Option<String> {
172
+ let children = viewer.children();
173
+ let plugin = (0..children.length())
174
+ .filter_map(|i| children.item(i))
175
+ .filter(|el| el.get_attribute("slot").as_deref() == Some(slot))
176
+ .find(|el| el.get_attribute("theme").as_deref() == Some(theme))?;
177
+
178
+ let value = web_sys::window()?
179
+ .get_computed_style(&plugin)
180
+ .ok()??
181
+ .get_property_value(BACKGROUND_VAR)
182
+ .ok()?;
183
+
184
+ let value = value.trim();
185
+ (!value.is_empty()).then(|| value.to_owned())
186
+ }
@@ -0,0 +1,62 @@
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
+ use crate::components::panel_menu::PanelCommand;
14
+
15
+ #[derive(Debug)]
16
+ pub enum MainPanelMsg {
17
+ PointerEvent(web_sys::PointerEvent),
18
+
19
+ /// The `Workspace` staged-panel set changed (`staged_changed` PubSub —
20
+ /// see `Workspace::stage_panel`): re-render so cells/tabs/wrappers
21
+ /// reflect it and `reconcile` inserts a promoted panel. Subscribed on
22
+ /// MainPanel's OWN scope — the root's `LayoutChanged` delivery proved
23
+ /// unreliable from element-API task contexts (promoted panels stranded
24
+ /// in their hidden wrappers), while this component's own messages
25
+ /// deliver in every observed context.
26
+ StagedChanged,
27
+
28
+ /// The `<regular-layout>` tree changed (fired by its
29
+ /// `regular-layout-update` event). Used to detect panels removed from
30
+ /// the layout (e.g. a frame's close button) so they can be disposed.
31
+ LayoutUpdated,
32
+
33
+ /// A tab was selected (fired by `regular-layout-select`, `detail.name`);
34
+ /// the selected panel becomes the active panel. This is
35
+ /// regular-layout's own selection signal, so it resolves the correct
36
+ /// panel within a stack.
37
+ TabSelected(String),
38
+
39
+ /// The layout is about to resize its cells (fired by the cancelable
40
+ /// `regular-layout-before-resize`). Carries the event so its
41
+ /// `PresizeDetail` can pre-size each panel's plugin to its target box
42
+ /// before the layout commits — avoiding the post-commit resize
43
+ /// shear/clip. The handler (`onbeforeresize` closure) has already
44
+ /// `preventDefault()`-ed it.
45
+ BeforeResize(web_sys::Event),
46
+
47
+ /// A right-click opened the panel context menu (target panel id, client
48
+ /// x, y). Fired by the imperative `contextmenu` listener on the panel
49
+ /// container — see `_contextmenu_listener` — and by each `PanelTab`.
50
+ /// `Some(id)` activates that panel and opens its
51
+ /// [`PanelMenu`](crate::components::panel_menu::PanelMenu) at the
52
+ /// cursor; `None` is the EMPTY stage (zero panels) — a target-less menu
53
+ /// offering only "New", whose items create the first panel.
54
+ ContextMenu(Option<String>, f64, f64),
55
+
56
+ /// Dismiss the panel context menu (the menu session ended).
57
+ CloseContextMenu,
58
+
59
+ /// The open context menu selected a command. Maximize/Restore act on the
60
+ /// layout here; the rest forward to `on_panel_command`.
61
+ Command(PanelCommand),
62
+ }
@@ -0,0 +1,220 @@
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 `MainPanelMsg::BeforeResize` handler: pre-size every panel's plugin to
14
+ //! its *target* cell box before the `<regular-layout>` commit, so content is
15
+ //! ready ahead of the geometry change (no post-commit shear/clip).
16
+
17
+ use std::collections::HashMap;
18
+
19
+ use futures::future::join_all;
20
+ use perspective_client::utils::PerspectiveResultExt;
21
+ use perspective_js::utils::JsValueSerdeExt;
22
+ use wasm_bindgen::prelude::*;
23
+ use yew::prelude::*;
24
+
25
+ use super::MainPanel;
26
+ use crate::ApiFuture;
27
+ use crate::js::{PresizeDetail, RegularLayout};
28
+ use crate::renderer::Renderer;
29
+ use crate::tasks::PanelResizeObserverHandle;
30
+ use crate::workspace::PanelId;
31
+
32
+ fn set_tab_presize(tab: &web_sys::HtmlElement, dx: f64, dy: f64) {
33
+ if dx.abs() > crate::renderer::SUBPIXEL_EPSILON || dy.abs() > crate::renderer::SUBPIXEL_EPSILON
34
+ {
35
+ let _ = tab
36
+ .style()
37
+ .set_property("transform", &format!("translate({dx}px, {dy}px)"));
38
+ }
39
+ }
40
+
41
+ /// Remove the inline presize translate applied by [`set_tab_presize`] (a no-op
42
+ /// when none was set). Cleared in the SAME synchronous run as the plugin clear
43
+ /// and `resumeResize`, so the tab and its plugin land at the committed grid
44
+ /// position together with no intermediate paint.
45
+ fn clear_tab_presize(tab: &web_sys::HtmlElement) {
46
+ let _ = tab.style().remove_property("transform");
47
+ }
48
+
49
+ #[derive(serde::Deserialize)]
50
+ struct PresizePath {
51
+ #[serde(with = "serde_wasm_bindgen::preserve")]
52
+ view_window: JsValue,
53
+ }
54
+
55
+ /// The current screen origin of a frame's grid TRACK (its margin box — the
56
+ /// coordinate space `real_coordinates` reports), measured live from the frame.
57
+ /// `target_track − current_track` is the presize translate delta: the plugin's
58
+ /// offset *within* its track is constant across a layout transition, so the
59
+ /// track delta is exactly the plugin delta (see `Renderer::presize_with_box`).
60
+ fn frame_track_origin(frame: &web_sys::Element) -> Option<(f64, f64)> {
61
+ let style = web_sys::window()?.get_computed_style(frame).ok()??;
62
+ let px = |prop: &str| {
63
+ style
64
+ .get_property_value(prop)
65
+ .ok()
66
+ .and_then(|v| v.trim().trim_end_matches("px").parse::<f64>().ok())
67
+ .unwrap_or(0.0)
68
+ };
69
+ let rect = frame.get_bounding_client_rect();
70
+ Some((
71
+ rect.left() - px("margin-left"),
72
+ rect.top() - px("margin-top"),
73
+ ))
74
+ }
75
+
76
+ impl MainPanel {
77
+ pub(super) fn on_before_resize(&mut self, ctx: &Context<Self>, event: web_sys::Event) -> bool {
78
+ let Some(el) = self.layout_ref.cast::<web_sys::HtmlElement>() else {
79
+ return false;
80
+ };
81
+
82
+ let layout: RegularLayout = el.unchecked_into();
83
+ let detail = event
84
+ .unchecked_ref::<web_sys::CustomEvent>()
85
+ .detail()
86
+ .unchecked_into::<PresizeDetail>();
87
+
88
+ let paths: HashMap<String, PresizePath> = detail
89
+ .calculate_presize_paths()
90
+ .into_serde_ext()
91
+ .unwrap_or_default();
92
+
93
+ // Bind a drag ResizeObserver on the panel being dragged: it's
94
+ // the one present in the layout but EXCLUDED from the presize
95
+ // paths. A STAGED panel is absent from the layout for a different
96
+ // reason than the dragged panel — never bind the per-drag observer
97
+ // to its staging wrapper.
98
+ for id in &ctx.props().panel_ids {
99
+ let name = id.as_str();
100
+ if !paths.contains_key(name)
101
+ && !self.panel_resize_observers.contains_key(name)
102
+ && !ctx.props().workspace.is_staged(id)
103
+ && let Some(panel) = ctx.props().workspace.panel(id)
104
+ && let Some(plugin) = panel.renderer.active_plugin()
105
+ {
106
+ let plugin_el = plugin.unchecked_ref::<web_sys::HtmlElement>();
107
+ self.panel_resize_observers.insert(
108
+ name.to_owned(),
109
+ PanelResizeObserverHandle::new(plugin_el, &panel.renderer),
110
+ );
111
+ }
112
+ }
113
+
114
+ let workspace = ctx.props().workspace.clone();
115
+ let viewer = ctx.props().presentation.viewer_elem().clone();
116
+ let effect = workspace.effects().guard();
117
+ ApiFuture::spawn(async move {
118
+ let _effect = effect;
119
+ let mut targets: Vec<(Renderer, f64, f64, f64, f64)> = Vec::new();
120
+ let mut tab_targets: Vec<(web_sys::HtmlElement, f64, f64)> = Vec::new();
121
+ let mut last_chrome: Option<(f64, f64)> = None;
122
+ for (name, path) in &paths {
123
+ let cell: web_sys::DomRect =
124
+ layout.real_coordinates(&path.view_window).unchecked_into();
125
+
126
+ // Only presize panels with a drawn plugin. Critically,
127
+ // `insertPanel`/`removePanel` *also* fire `before-resize`
128
+ // (they `restore()` through the presize queue), and a
129
+ // just-inserted panel has no plugin yet — presizing it
130
+ // would stall on the draw lock and `resumeResize` would
131
+ // never fire, freezing the layout mid-mutation. (Mirrors
132
+ // the old workspace's `if (!plugin_box) continue`.)
133
+ if let Some(panel) = workspace.panel(&PanelId::from(name.as_str()))
134
+ && panel.renderer.is_plugin_activated().unwrap_or(false)
135
+ && let Some(plugin) = panel.renderer.active_plugin()
136
+ {
137
+ let plugin_el = plugin.unchecked_ref::<web_sys::Element>();
138
+ let frame = layout
139
+ .unchecked_ref::<web_sys::Element>()
140
+ .query_selector(&format!("regular-layout-frame[name=\"{name}\"]"))
141
+ .ok()
142
+ .flatten();
143
+ let chrome = frame
144
+ .as_ref()
145
+ .and_then(|frame| crate::tasks::plugin_chrome(frame, plugin_el));
146
+ last_chrome = chrome.or(last_chrome);
147
+ let (cw, ch) = last_chrome.unwrap_or(crate::tasks::CHROME_FALLBACK);
148
+ let width = (cell.width() - cw).max(0.0);
149
+ let height = (cell.height() - ch).max(0.0);
150
+ let (dx, dy) = frame
151
+ .as_ref()
152
+ .and_then(frame_track_origin)
153
+ .map(|(x, y)| (cell.left() - x, cell.top() - y))
154
+ .unwrap_or((0.0, 0.0));
155
+ targets.push((panel.renderer, dx, dy, width, height));
156
+
157
+ if let Some(tab) = viewer
158
+ .query_selector(&format!("perspective-viewer-tab[slot=\"tab-{name}\"]"))
159
+ .ok()
160
+ .flatten()
161
+ {
162
+ tab_targets.push((tab.unchecked_into::<web_sys::HtmlElement>(), dx, dy));
163
+ }
164
+ }
165
+ }
166
+
167
+ let renderers = targets.iter().map(|(r, ..)| r.clone()).collect::<Vec<_>>();
168
+ let tabs = tab_targets
169
+ .iter()
170
+ .map(|(t, ..)| t.clone())
171
+ .collect::<Vec<_>>();
172
+ for (tab, dx, dy) in &tab_targets {
173
+ set_tab_presize(tab, *dx, *dy);
174
+ }
175
+
176
+ let presents = crate::tasks::staged_presents_with_deadline(
177
+ workspace.effects().guard(),
178
+ async move {
179
+ join_all(
180
+ targets
181
+ .iter()
182
+ .map(|(r, dx, dy, w, h)| r.presize_with_box(*dx, *dy, *w, *h)),
183
+ )
184
+ .await
185
+ .into_iter()
186
+ .filter_map(|r| r.ok().flatten())
187
+ .collect()
188
+ },
189
+ {
190
+ let renderers = renderers.clone();
191
+ let tabs = tabs.clone();
192
+ move || {
193
+ for renderer in &renderers {
194
+ let _ = renderer.clear_presize();
195
+ }
196
+
197
+ for tab in &tabs {
198
+ clear_tab_presize(tab);
199
+ }
200
+ }
201
+ },
202
+ )
203
+ .await;
204
+
205
+ for renderer in &renderers {
206
+ renderer.clear_presize().unwrap_or_log();
207
+ }
208
+
209
+ for tab in &tabs {
210
+ clear_tab_presize(tab);
211
+ }
212
+
213
+ presents.reveal();
214
+ layout.resume_resize();
215
+ Ok(())
216
+ });
217
+
218
+ false
219
+ }
220
+ }