@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
|
@@ -13,26 +13,62 @@
|
|
|
13
13
|
//! Theme reset / set task.
|
|
14
14
|
|
|
15
15
|
use futures::future::join_all;
|
|
16
|
+
use perspective_client::clone;
|
|
16
17
|
use perspective_js::utils::*;
|
|
17
18
|
|
|
18
19
|
use crate::presentation::Presentation;
|
|
19
|
-
use crate::renderer::Renderer;
|
|
20
|
+
use crate::renderer::{Renderer, StagedEdit};
|
|
21
|
+
use crate::session::{EditDelta, OpKind, Session};
|
|
20
22
|
use crate::workspace::Workspace;
|
|
21
23
|
|
|
22
24
|
/// Give `renderer` the concrete registry default IF it has none yet — the
|
|
23
25
|
/// cold-boot case, where the panel was created before the registry first
|
|
24
26
|
/// parsed and `active_theme_name_sync` had nothing to resolve. Only ever
|
|
25
27
|
/// FILLS IN, never overwrites, so it cannot repaint a themed panel.
|
|
26
|
-
pub(crate) async fn seed_panel_theme(
|
|
28
|
+
pub(crate) async fn seed_panel_theme(
|
|
29
|
+
presentation: &Presentation,
|
|
30
|
+
session: &Session,
|
|
31
|
+
renderer: &Renderer,
|
|
32
|
+
) {
|
|
27
33
|
if renderer.theme().is_none() {
|
|
28
|
-
|
|
34
|
+
submit_theme(
|
|
35
|
+
session,
|
|
36
|
+
renderer,
|
|
37
|
+
presentation.get_default_theme_name().await,
|
|
38
|
+
);
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
/// Submit a theme pick as a UI edit, visible to the UI from this call.
|
|
43
|
+
pub fn submit_theme(session: &Session, renderer: &Renderer, theme: Option<String>) {
|
|
44
|
+
let staged = renderer.stage(StagedEdit::Theme(theme.clone()));
|
|
45
|
+
let kind = OpKind::Edit {
|
|
46
|
+
delta: EditDelta::Renderer,
|
|
47
|
+
swaps_plugin: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let _ticket = session.submit(kind, {
|
|
51
|
+
clone!(renderer);
|
|
52
|
+
move |_ctx| {
|
|
53
|
+
Box::pin(async move {
|
|
54
|
+
renderer.commit_theme(theme);
|
|
55
|
+
drop(staged);
|
|
56
|
+
Ok(None)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// [`submit_theme`] plus a synchronous [`Renderer::stamp_theme`].
|
|
63
|
+
pub fn submit_theme_stamped(session: &Session, renderer: &Renderer, theme: Option<String>) {
|
|
64
|
+
submit_theme(session, renderer, theme);
|
|
65
|
+
renderer.stamp_theme(None);
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
/// [`seed_panel_theme`] for every panel.
|
|
33
69
|
pub(crate) async fn seed_default_themes(presentation: &Presentation, workspace: &Workspace) {
|
|
34
70
|
for panel in workspace.panels() {
|
|
35
|
-
seed_panel_theme(presentation, &panel.renderer).await;
|
|
71
|
+
seed_panel_theme(presentation, &panel.session, &panel.renderer).await;
|
|
36
72
|
}
|
|
37
73
|
}
|
|
38
74
|
|
|
@@ -55,6 +91,7 @@ pub(crate) async fn seed_default_themes(presentation: &Presentation, workspace:
|
|
|
55
91
|
/// stamped at the plugin's last capture): the picked panel when the value
|
|
56
92
|
/// is genuinely new, and never a panel that has yet to first-paint.
|
|
57
93
|
pub fn update_theme(
|
|
94
|
+
session: &Session,
|
|
58
95
|
renderer: &Renderer,
|
|
59
96
|
presentation: &Presentation,
|
|
60
97
|
workspace: &Workspace,
|
|
@@ -65,12 +102,13 @@ pub fn update_theme(
|
|
|
65
102
|
// cascade styles. Only "reset to default" has to await the registry, and
|
|
66
103
|
// it records the resolved NAME, never an empty theme.
|
|
67
104
|
if let Some(name) = &theme {
|
|
68
|
-
renderer
|
|
105
|
+
submit_theme_stamped(session, renderer, Some(name.clone()));
|
|
69
106
|
}
|
|
70
107
|
|
|
71
108
|
let presentation = presentation.clone();
|
|
72
109
|
let workspace = workspace.clone();
|
|
73
110
|
let renderer = renderer.clone();
|
|
111
|
+
let session = session.clone();
|
|
74
112
|
ApiFuture::spawn(async move {
|
|
75
113
|
match theme {
|
|
76
114
|
Some(name) => {
|
|
@@ -78,7 +116,8 @@ pub fn update_theme(
|
|
|
78
116
|
},
|
|
79
117
|
None => {
|
|
80
118
|
presentation.reset_theme().await?;
|
|
81
|
-
|
|
119
|
+
let default = presentation.get_default_theme_name().await;
|
|
120
|
+
submit_theme_stamped(&session, &renderer, default);
|
|
82
121
|
},
|
|
83
122
|
}
|
|
84
123
|
|
|
@@ -195,6 +195,22 @@ impl Component for ScrollPanel {
|
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
let width_style = format!("width:{}px", self.viewport_width.max(0.0));
|
|
198
|
+
let mut rows = windowed_items
|
|
199
|
+
.iter()
|
|
200
|
+
.cloned()
|
|
201
|
+
.map(Html::from)
|
|
202
|
+
.collect::<Vec<_>>();
|
|
203
|
+
|
|
204
|
+
if !ctx.props().omit_autosize_div {
|
|
205
|
+
rows.push(html! {
|
|
206
|
+
<div
|
|
207
|
+
key="__scroll-panel-auto-width__"
|
|
208
|
+
class="scroll-panel-auto-width"
|
|
209
|
+
style={width_style}
|
|
210
|
+
/>
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
198
214
|
let items = if !windowed_items.is_empty() {
|
|
199
215
|
let onscroll = ctx.link().batch_callback(|_| {
|
|
200
216
|
vec![
|
|
@@ -214,16 +230,7 @@ impl Component for ScrollPanel {
|
|
|
214
230
|
ondrop={&ctx.props().drop}
|
|
215
231
|
class={ctx.props().class.clone()}
|
|
216
232
|
>
|
|
217
|
-
<div class="scroll-panel-container" style={window_style}>
|
|
218
|
-
{ for windowed_items.iter().cloned().map(Html::from) }
|
|
219
|
-
if !ctx.props().omit_autosize_div {
|
|
220
|
-
<div
|
|
221
|
-
key="__scroll-panel-auto-width__"
|
|
222
|
-
class="scroll-panel-auto-width"
|
|
223
|
-
style={width_style}
|
|
224
|
-
/>
|
|
225
|
-
}
|
|
226
|
-
</div>
|
|
233
|
+
<div class="scroll-panel-container" style={window_style}>{ for rows }</div>
|
|
227
234
|
<div class="scroll-panel-content" style={content_style} />
|
|
228
235
|
</div>
|
|
229
236
|
}
|
|
@@ -496,44 +496,3 @@ impl ResizingState {
|
|
|
496
496
|
Ok(self.body_style.set_property("cursor", &self.cursor)?)
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
|
-
|
|
500
|
-
#[cfg(test)]
|
|
501
|
-
mod tests {
|
|
502
|
-
use super::*;
|
|
503
|
-
|
|
504
|
-
#[test]
|
|
505
|
-
fn html_if_empty_branch_is_not_literally_empty() {
|
|
506
|
-
let via_if: Html = html! { if false { <div /> } else { <></> } };
|
|
507
|
-
assert_ne!(via_if, html! { <></> });
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
#[test]
|
|
511
|
-
fn empty_fragment_is_empty() {
|
|
512
|
-
assert!(is_empty_html(&html! { <></> }));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
#[test]
|
|
516
|
-
fn html_if_empty_branch_is_semantically_empty() {
|
|
517
|
-
assert!(is_empty_html(&html! { if false { <div/> } else { <></> } }));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
#[test]
|
|
521
|
-
fn nested_empty_fragments_are_empty() {
|
|
522
|
-
assert!(is_empty_html(&html! { <><><></></></> }));
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
#[test]
|
|
526
|
-
fn tag_is_not_empty() {
|
|
527
|
-
assert!(!is_empty_html(&html! { <div/> }));
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
#[test]
|
|
531
|
-
fn fragment_containing_a_tag_is_not_empty() {
|
|
532
|
-
assert!(!is_empty_html(&html! { <><div/></> }));
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
#[test]
|
|
536
|
-
fn text_is_not_empty() {
|
|
537
|
-
assert!(!is_empty_html(&html! { { "" } }));
|
|
538
|
-
}
|
|
539
|
-
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
use
|
|
13
|
+
use perspective_js::utils::global;
|
|
14
|
+
use wasm_bindgen::JsCast;
|
|
15
|
+
use web_sys::{HtmlElement, MouseEvent, Node};
|
|
14
16
|
use yew::prelude::*;
|
|
15
17
|
|
|
16
18
|
/// A single command row in a `ContextMenu`.
|
|
@@ -101,25 +103,162 @@ fn render_entry(entry: &ContextMenuEntry, on_close: &Callback<()>) -> Html {
|
|
|
101
103
|
on_select,
|
|
102
104
|
entries,
|
|
103
105
|
} => {
|
|
104
|
-
let onclick = {
|
|
105
|
-
let on_select = on_select.clone();
|
|
106
|
-
let on_close = on_close.clone();
|
|
107
|
-
Callback::from(move |e: MouseEvent| {
|
|
108
|
-
e.stop_propagation();
|
|
109
|
-
if let Some(on_select) = &on_select {
|
|
110
|
-
on_select.emit(());
|
|
111
|
-
on_close.emit(());
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
let children = entries.iter().map(|entry| render_entry(entry, on_close));
|
|
117
106
|
html! {
|
|
118
|
-
<
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
<Submenu
|
|
108
|
+
label={label.clone()}
|
|
109
|
+
on_select={on_select.clone()}
|
|
110
|
+
entries={entries.clone()}
|
|
111
|
+
on_close={on_close.clone()}
|
|
112
|
+
/>
|
|
122
113
|
}
|
|
123
114
|
},
|
|
124
115
|
}
|
|
125
116
|
}
|
|
117
|
+
|
|
118
|
+
#[derive(Properties, PartialEq)]
|
|
119
|
+
pub struct SubmenuProps {
|
|
120
|
+
pub label: String,
|
|
121
|
+
pub on_select: Option<Callback<()>>,
|
|
122
|
+
pub entries: Vec<ContextMenuEntry>,
|
|
123
|
+
pub on_close: Callback<()>,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub enum SubmenuMsg {
|
|
127
|
+
Enter,
|
|
128
|
+
Leave,
|
|
129
|
+
Measure,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// A hover-opened flyout row that opens toward whichever side of its parent
|
|
133
|
+
/// has room for it, tracked through the bubbling `mouseover`/`mouseout` pair
|
|
134
|
+
/// because Chrome withholds `mouseenter` from listeners on a shadow root.
|
|
135
|
+
pub struct Submenu {
|
|
136
|
+
flyout: NodeRef,
|
|
137
|
+
open: bool,
|
|
138
|
+
flip_x: bool,
|
|
139
|
+
flip_y: bool,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl Submenu {
|
|
143
|
+
/// Which sides the flyout must open toward to stay on-screen, or `None`
|
|
144
|
+
/// while it is hidden and has no size to measure.
|
|
145
|
+
fn decide(&self) -> Option<(bool, bool)> {
|
|
146
|
+
let flyout = self.flyout.cast::<HtmlElement>()?;
|
|
147
|
+
let item = flyout.parent_element()?;
|
|
148
|
+
let flyout_rect = flyout.get_bounding_client_rect();
|
|
149
|
+
if flyout_rect.width() == 0.0 {
|
|
150
|
+
return None;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let window = global::window();
|
|
154
|
+
let inner_width = window.inner_width().unwrap().as_f64().unwrap();
|
|
155
|
+
let inner_height = window.inner_height().unwrap().as_f64().unwrap();
|
|
156
|
+
let item_rect = item.get_bounding_client_rect();
|
|
157
|
+
let over_x = inner_width < item_rect.right() + flyout_rect.width();
|
|
158
|
+
let over_y = inner_height < item_rect.top() - 1.0 + flyout_rect.height();
|
|
159
|
+
let flip_x = over_x && item_rect.left() > inner_width - item_rect.right();
|
|
160
|
+
let flip_y = over_y && item_rect.bottom() > inner_height - item_rect.top();
|
|
161
|
+
Some((flip_x, flip_y))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
impl Component for Submenu {
|
|
166
|
+
type Message = SubmenuMsg;
|
|
167
|
+
type Properties = SubmenuProps;
|
|
168
|
+
|
|
169
|
+
fn create(_ctx: &Context<Self>) -> Self {
|
|
170
|
+
Self {
|
|
171
|
+
flyout: NodeRef::default(),
|
|
172
|
+
open: false,
|
|
173
|
+
flip_x: false,
|
|
174
|
+
flip_y: false,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
179
|
+
match msg {
|
|
180
|
+
SubmenuMsg::Enter => {
|
|
181
|
+
let changed = !self.open;
|
|
182
|
+
self.open = true;
|
|
183
|
+
changed
|
|
184
|
+
},
|
|
185
|
+
SubmenuMsg::Leave => {
|
|
186
|
+
let changed = self.open;
|
|
187
|
+
self.open = false;
|
|
188
|
+
changed
|
|
189
|
+
},
|
|
190
|
+
SubmenuMsg::Measure => match self.decide() {
|
|
191
|
+
Some((flip_x, flip_y)) => {
|
|
192
|
+
let changed = (flip_x, flip_y) != (self.flip_x, self.flip_y);
|
|
193
|
+
self.flip_x = flip_x;
|
|
194
|
+
self.flip_y = flip_y;
|
|
195
|
+
changed
|
|
196
|
+
},
|
|
197
|
+
None => false,
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
|
|
203
|
+
if self.open
|
|
204
|
+
&& self
|
|
205
|
+
.decide()
|
|
206
|
+
.is_some_and(|d| d != (self.flip_x, self.flip_y))
|
|
207
|
+
{
|
|
208
|
+
ctx.link().send_message(SubmenuMsg::Measure);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
213
|
+
let props = ctx.props();
|
|
214
|
+
let onclick = {
|
|
215
|
+
let on_select = props.on_select.clone();
|
|
216
|
+
let on_close = props.on_close.clone();
|
|
217
|
+
Callback::from(move |e: MouseEvent| {
|
|
218
|
+
e.stop_propagation();
|
|
219
|
+
if let Some(on_select) = &on_select {
|
|
220
|
+
on_select.emit(());
|
|
221
|
+
on_close.emit(());
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
let onmouseover = ctx.link().callback(|_: MouseEvent| SubmenuMsg::Enter);
|
|
227
|
+
let onmouseout = ctx.link().batch_callback({
|
|
228
|
+
let flyout = self.flyout.clone();
|
|
229
|
+
move |event: MouseEvent| {
|
|
230
|
+
let item = flyout.cast::<HtmlElement>()?.parent_element()?;
|
|
231
|
+
let related = event
|
|
232
|
+
.related_target()
|
|
233
|
+
.and_then(|target| target.dyn_into::<Node>().ok());
|
|
234
|
+
|
|
235
|
+
let stays = related.is_some_and(|node| item.contains(Some(&node)));
|
|
236
|
+
(!stays).then_some(SubmenuMsg::Leave)
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
let mut class = classes!("context-menu", "context-menu-submenu");
|
|
240
|
+
if self.open {
|
|
241
|
+
class.push("open");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if self.flip_x {
|
|
245
|
+
class.push("flip-x");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if self.flip_y {
|
|
249
|
+
class.push("flip-y");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let children = props
|
|
253
|
+
.entries
|
|
254
|
+
.iter()
|
|
255
|
+
.map(|entry| render_entry(entry, &props.on_close));
|
|
256
|
+
|
|
257
|
+
html! {
|
|
258
|
+
<span class="context-menu-item has-submenu" {onclick} {onmouseover} {onmouseout}>
|
|
259
|
+
{ props.label.clone() }
|
|
260
|
+
<div {class} ref={self.flyout.clone()}>{ for children }</div>
|
|
261
|
+
</span>
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -12,13 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
use std::cell::{Cell, Ref, RefCell};
|
|
14
14
|
use std::future::Future;
|
|
15
|
-
use std::iter::{Iterator, repeat_with};
|
|
16
15
|
use std::rc::Rc;
|
|
17
16
|
|
|
18
17
|
use futures::future::{join_all, select_all};
|
|
19
18
|
use perspective_js::utils::{global, *};
|
|
20
19
|
use wasm_bindgen::prelude::*;
|
|
21
|
-
use wasm_bindgen::{JsCast, intern};
|
|
22
20
|
use wasm_bindgen_futures::JsFuture;
|
|
23
21
|
use yew::prelude::*;
|
|
24
22
|
|
|
@@ -119,31 +117,15 @@ impl FontLoaderProps {
|
|
|
119
117
|
self.state.on_update.emit(());
|
|
120
118
|
|
|
121
119
|
for (family, weight) in preload_fonts.iter() {
|
|
122
|
-
let
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let font_face = js_sys::Reflect::get(&entry, &intern("value").into())?
|
|
127
|
-
.dyn_into::<web_sys::FontFace>()?;
|
|
128
|
-
|
|
129
|
-
if family == &font_face.family().replace('"', "")
|
|
130
|
-
&& (weight == &font_face.weight()
|
|
131
|
-
|| (font_face.weight() == "normal" && weight == "400"))
|
|
132
|
-
{
|
|
133
|
-
block_fonts.push(ApiFuture::new(async move {
|
|
134
|
-
Ok(JsFuture::from(font_face.loaded()?).await?)
|
|
135
|
-
}));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
120
|
+
let block_fonts: PromiseSet = vec![
|
|
121
|
+
ApiFuture::new(timeout_font_task(family, weight)),
|
|
122
|
+
ApiFuture::new(await_font(family.clone(), weight.clone())),
|
|
123
|
+
];
|
|
138
124
|
|
|
139
125
|
let fut = async { select_all(block_fonts).await.0 };
|
|
140
126
|
block_promises.push(ApiFuture::new(fut))
|
|
141
127
|
}
|
|
142
128
|
|
|
143
|
-
if block_promises.len() != preload_fonts.len() {
|
|
144
|
-
web_sys::console::warn_1(&format!("Missing preload fonts {:?}", preload_fonts).into());
|
|
145
|
-
}
|
|
146
|
-
|
|
147
129
|
let res = join_all(block_promises)
|
|
148
130
|
.await
|
|
149
131
|
.into_iter()
|
|
@@ -156,6 +138,25 @@ impl FontLoaderProps {
|
|
|
156
138
|
}
|
|
157
139
|
}
|
|
158
140
|
|
|
141
|
+
/// Resolve once the `@font-face`s needed to render [`FONT_TEST_SAMPLE`] in
|
|
142
|
+
/// `(family, weight)` are loaded — `FontFaceSet.load(spec, text)` both
|
|
143
|
+
/// triggers and awaits exactly those SUBSET faces, never a sibling
|
|
144
|
+
/// unicode-range face the page may never use. It resolves EMPTY while the
|
|
145
|
+
/// family has no registration at all (its stylesheet may still be fetching,
|
|
146
|
+
/// and Firefox registers lazily-parsed faces later than Chrome), so poll.
|
|
147
|
+
async fn await_font(family: String, weight: String) -> ApiResult<JsValue> {
|
|
148
|
+
let spec = format!("{weight} 12px \"{family}\"");
|
|
149
|
+
loop {
|
|
150
|
+
let fonts = global::document().fonts();
|
|
151
|
+
let loaded = JsFuture::from(fonts.load_with_text(&spec, FONT_TEST_SAMPLE)).await?;
|
|
152
|
+
if js_sys::Array::from(&loaded).length() > 0 {
|
|
153
|
+
return Ok(JsValue::UNDEFINED);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
set_timeout(100).await?;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
159
160
|
fn timeout_font_task(
|
|
160
161
|
family: &str,
|
|
161
162
|
weight: &str,
|
|
@@ -198,16 +199,3 @@ fn parse_fonts(txt: &str) -> Vec<(String, String)> {
|
|
|
198
199
|
.flatten()
|
|
199
200
|
.collect::<Vec<_>>()
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
-
fn font_iter(
|
|
203
|
-
iter: web_sys::FontFaceSetIterator,
|
|
204
|
-
) -> impl Iterator<Item = web_sys::FontFaceSetIteratorResult> {
|
|
205
|
-
repeat_with(move || iter.next())
|
|
206
|
-
.filter_map(|x| x.ok())
|
|
207
|
-
.take_while(|entry| {
|
|
208
|
-
!js_sys::Reflect::get(entry, &intern("done").into())
|
|
209
|
-
.unwrap()
|
|
210
|
-
.as_bool()
|
|
211
|
-
.unwrap()
|
|
212
|
-
})
|
|
213
|
-
}
|
|
@@ -98,15 +98,3 @@ pub fn MirroredTextarea(props: &MirroredTextareaProps) -> Html {
|
|
|
98
98
|
</div>
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
#[cfg(test)]
|
|
103
|
-
mod tests {
|
|
104
|
-
use super::*;
|
|
105
|
-
|
|
106
|
-
#[test]
|
|
107
|
-
fn mirror_never_compares_equal() {
|
|
108
|
-
let mirror = Mirror(Html::default());
|
|
109
|
-
assert!(mirror != mirror.clone());
|
|
110
|
-
assert!(Mirror(Html::default()) != Mirror(Html::default()));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
@@ -384,92 +384,3 @@ pub fn multi_stop_gradient_selector(props: &MultiStopGradientProps) -> Html {
|
|
|
384
384
|
</>
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
|
-
|
|
388
|
-
#[cfg(test)]
|
|
389
|
-
mod tests {
|
|
390
|
-
use super::*;
|
|
391
|
-
|
|
392
|
-
fn stop(color: &str, offset: f64) -> GradientStopSpec {
|
|
393
|
-
GradientStopSpec {
|
|
394
|
-
color: color.to_owned(),
|
|
395
|
-
offset,
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
#[test]
|
|
400
|
-
fn sample_clamps_outside_stop_range() {
|
|
401
|
-
let stops = vec![stop("#000000", 0.25), stop("#ff0000", 0.75)];
|
|
402
|
-
assert_eq!(sample_gradient_hex(&stops, 0.0), "#000000");
|
|
403
|
-
assert_eq!(sample_gradient_hex(&stops, 1.0), "#ff0000");
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
#[test]
|
|
407
|
-
fn sample_interpolates_linearly() {
|
|
408
|
-
let stops = vec![stop("#000000", 0.0), stop("#ff0000", 1.0)];
|
|
409
|
-
assert_eq!(sample_gradient_hex(&stops, 0.5), "#800000");
|
|
410
|
-
|
|
411
|
-
let stops = vec![
|
|
412
|
-
stop("#000000", 0.0),
|
|
413
|
-
stop("#ffffff", 0.5),
|
|
414
|
-
stop("#ff0000", 1.0),
|
|
415
|
-
];
|
|
416
|
-
|
|
417
|
-
assert_eq!(sample_gradient_hex(&stops, 0.25), "#808080");
|
|
418
|
-
assert_eq!(sample_gradient_hex(&stops, 0.75), "#ff8080");
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
#[test]
|
|
422
|
-
fn track_positions_pad_the_bar_edges() {
|
|
423
|
-
assert_eq!(track_position(0.0), "calc(8px + (100% - 16px) * 0.0000)");
|
|
424
|
-
assert_eq!(track_position(1.0), "calc(8px + (100% - 16px) * 1.0000)");
|
|
425
|
-
assert_eq!(track_position(0.333), "calc(8px + (100% - 16px) * 0.3330)");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
#[test]
|
|
429
|
-
fn gradient_interpolates_on_padded_track() {
|
|
430
|
-
let stops = vec![
|
|
431
|
-
stop("#333333", 1.0),
|
|
432
|
-
stop("#111111", 0.0),
|
|
433
|
-
stop("#222222", 0.5),
|
|
434
|
-
];
|
|
435
|
-
|
|
436
|
-
assert_eq!(
|
|
437
|
-
css_gradient(&stops),
|
|
438
|
-
format!(
|
|
439
|
-
"background:linear-gradient(to right, #111111 {}, #222222 {}, #333333 {})",
|
|
440
|
-
track_position(0.0),
|
|
441
|
-
track_position(0.5),
|
|
442
|
-
track_position(1.0)
|
|
443
|
-
)
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
#[test]
|
|
448
|
-
fn equalize_centers_between_neighbors_or_edges() {
|
|
449
|
-
let stops = vec![
|
|
450
|
-
stop("#111111", 0.2),
|
|
451
|
-
stop("#222222", 0.25),
|
|
452
|
-
stop("#333333", 0.4),
|
|
453
|
-
];
|
|
454
|
-
|
|
455
|
-
assert_eq!(equalized_offset(&stops, 1), (0.2 + 0.4) / 2.0);
|
|
456
|
-
|
|
457
|
-
assert_eq!(equalized_offset(&stops, 0), 0.125);
|
|
458
|
-
assert_eq!(equalized_offset(&stops, 2), (0.25 + 1.0) / 2.0);
|
|
459
|
-
|
|
460
|
-
let single = vec![stop("#111111", 0.9)];
|
|
461
|
-
assert_eq!(equalized_offset(&single, 0), 0.5);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
#[test]
|
|
465
|
-
fn sample_degenerate_cases() {
|
|
466
|
-
assert_eq!(sample_gradient_hex(&[], 0.5), "#000000");
|
|
467
|
-
let stops = vec![stop("#123456", 0.5)];
|
|
468
|
-
assert_eq!(sample_gradient_hex(&stops, 0.1), "#123456");
|
|
469
|
-
assert_eq!(sample_gradient_hex(&stops, 0.9), "#123456");
|
|
470
|
-
|
|
471
|
-
let stops = vec![stop("red", 0.0), stop("blue", 1.0)];
|
|
472
|
-
assert_eq!(sample_gradient_hex(&stops, 0.25), "red");
|
|
473
|
-
assert_eq!(sample_gradient_hex(&stops, 0.75), "blue");
|
|
474
|
-
}
|
|
475
|
-
}
|
|
@@ -386,54 +386,3 @@ pub fn palette_selector(props: &PaletteProps) -> Html {
|
|
|
386
386
|
</>
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
|
-
|
|
390
|
-
#[cfg(test)]
|
|
391
|
-
mod tests {
|
|
392
|
-
use super::*;
|
|
393
|
-
|
|
394
|
-
fn v(items: &[&str]) -> Vec<String> {
|
|
395
|
-
items.iter().map(|x| x.to_string()).collect()
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
#[test]
|
|
399
|
-
fn insertion_index_splits_at_centers_and_resolves_rows() {
|
|
400
|
-
let centers = [(12.0, 12.0), (40.0, 12.0), (68.0, 12.0), (12.0, 40.0)];
|
|
401
|
-
assert_eq!(insertion_index(¢ers, 0.0, 12.0), 0);
|
|
402
|
-
assert_eq!(insertion_index(¢ers, 13.0, 12.0), 1);
|
|
403
|
-
assert_eq!(insertion_index(¢ers, 39.0, 12.0), 1);
|
|
404
|
-
assert_eq!(insertion_index(¢ers, 41.0, 12.0), 2);
|
|
405
|
-
assert_eq!(insertion_index(¢ers, 90.0, 12.0), 3);
|
|
406
|
-
assert_eq!(insertion_index(¢ers, 10.0, 41.0), 3);
|
|
407
|
-
assert_eq!(insertion_index(¢ers, 30.0, 41.0), 4);
|
|
408
|
-
assert_eq!(insertion_index(&[], 5.0, 5.0), 0);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
#[test]
|
|
412
|
-
fn slot_from_display_maps_previewed_slots_back_to_original_order() {
|
|
413
|
-
assert_eq!(slot_from_display(3, 2, 0), 3);
|
|
414
|
-
assert_eq!(slot_from_display(2, 2, 0), 3);
|
|
415
|
-
assert_eq!(slot_from_display(1, 2, 0), 2);
|
|
416
|
-
assert_eq!(slot_from_display(0, 2, 0), 0);
|
|
417
|
-
assert_eq!(slot_from_display(0, 0, 2), 0);
|
|
418
|
-
assert_eq!(slot_from_display(1, 0, 2), 0);
|
|
419
|
-
assert_eq!(slot_from_display(2, 0, 2), 1);
|
|
420
|
-
assert_eq!(slot_from_display(3, 0, 2), 2);
|
|
421
|
-
assert_eq!(slot_from_display(4, 0, 2), 4);
|
|
422
|
-
assert_eq!(slot_from_display(1, 1, 1), 1);
|
|
423
|
-
assert_eq!(slot_from_display(2, 1, 1), 1);
|
|
424
|
-
assert_eq!(slot_from_display(3, 1, 1), 3);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
#[test]
|
|
428
|
-
fn reordered_moves_and_rejects_no_ops() {
|
|
429
|
-
let values = v(&["a", "b", "c", "d"]);
|
|
430
|
-
assert_eq!(reordered(&values, 0, 4), Some(v(&["b", "c", "d", "a"])));
|
|
431
|
-
assert_eq!(reordered(&values, 3, 0), Some(v(&["d", "a", "b", "c"])));
|
|
432
|
-
assert_eq!(reordered(&values, 0, 2), Some(v(&["b", "a", "c", "d"])));
|
|
433
|
-
assert_eq!(reordered(&values, 2, 1), Some(v(&["a", "c", "b", "d"])));
|
|
434
|
-
assert_eq!(reordered(&values, 1, 1), None);
|
|
435
|
-
assert_eq!(reordered(&values, 1, 2), None);
|
|
436
|
-
assert_eq!(reordered(&values, 4, 0), None);
|
|
437
|
-
assert_eq!(reordered(&values, 0, 5), None);
|
|
438
|
-
}
|
|
439
|
-
}
|
package/src/rust/ui/portal.rs
CHANGED
|
@@ -135,19 +135,13 @@ impl PortalModal {
|
|
|
135
135
|
|
|
136
136
|
fn position_against_target(&mut self, target: &HtmlElement) {
|
|
137
137
|
let target_rect = target.get_bounding_client_rect();
|
|
138
|
-
let height = target_rect.height();
|
|
139
|
-
let width = target_rect.width();
|
|
140
|
-
let top = target_rect.top();
|
|
141
|
-
let left = target_rect.left();
|
|
142
|
-
|
|
143
138
|
if !self.visible {
|
|
144
|
-
self.top = top + height - 1.0;
|
|
145
|
-
self.left = left;
|
|
146
|
-
self.visible = false;
|
|
139
|
+
self.top = target_rect.top() + target_rect.height() - 1.0;
|
|
140
|
+
self.left = target_rect.left();
|
|
147
141
|
} else {
|
|
148
|
-
let anchor = calc_relative_position(&self.host, top, left, height, width);
|
|
149
|
-
self.anchor.set(anchor);
|
|
150
142
|
let modal_rect = self.host.get_bounding_client_rect();
|
|
143
|
+
let anchor = calc_relative_position(&target_rect, &modal_rect);
|
|
144
|
+
self.anchor.set(anchor);
|
|
151
145
|
let (new_top, new_left) = calc_anchor_position(anchor, &target_rect, &modal_rect);
|
|
152
146
|
self.top = new_top;
|
|
153
147
|
self.left = new_left;
|
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
mod blob;
|
|
16
16
|
mod download;
|
|
17
17
|
mod dragdrop;
|
|
18
|
+
mod mouse;
|
|
18
19
|
mod request_animation_frame;
|
|
19
20
|
mod selection;
|
|
20
21
|
|
|
21
|
-
#[cfg(test)]
|
|
22
|
-
mod tests;
|
|
23
|
-
|
|
24
22
|
pub use self::blob::*;
|
|
25
23
|
pub use self::download::*;
|
|
26
24
|
pub use self::dragdrop::*;
|
|
25
|
+
pub use self::mouse::*;
|
|
27
26
|
pub use self::request_animation_frame::*;
|
|
28
27
|
pub use self::selection::*;
|