@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.
Files changed (194) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -10,14 +10,15 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- import perspective_viewer from "./perspective-viewer.ts";
14
- export * from "./perspective-viewer.ts";
13
+ use web_sys::MouseEvent;
14
+ use yew::Callback;
15
15
 
16
- // @ts-ignore
17
- import client_wasm from "../../dist/wasm/perspective-viewer.wasm";
18
-
19
- await perspective_viewer.init_client(client_wasm as any as ArrayBuffer);
20
-
21
- console.warn("Perspective wasn been initialized in inline mode");
22
-
23
- export default perspective_viewer;
16
+ /// Gate a `mousedown` callback to the primary button, so a right-click (which
17
+ /// opens the viewer context menu) never also triggers the control under it.
18
+ pub fn left_click(callback: Callback<MouseEvent>) -> Callback<MouseEvent> {
19
+ Callback::from(move |event: MouseEvent| {
20
+ if event.button() == 0 {
21
+ callback.emit(event);
22
+ }
23
+ })
24
+ }
@@ -49,15 +49,34 @@ pub async fn request_animation_frame() {
49
49
 
50
50
  /// An `async` which awaits the browser's `load` event, which is automatically
51
51
  /// bypassed if `document.readyState` indicates this has already occurred.
52
+ /// Resolve once the DOM is interactive and every linked stylesheet has
53
+ /// APPLIED (`link.sheet` set) — the point at which computed styles are
54
+ /// readable. Never await the `load` EVENT here: an in-flight `@font-face`
55
+ /// request delays `load` on Firefox, and the font loader that depends on
56
+ /// this function is itself what manages those requests.
52
57
  pub async fn await_dom_loaded() -> ApiResult<()> {
53
- let state = global::document().ready_state();
54
- if state == "complete" || state == "loaded" {
55
- Ok(())
56
- } else {
57
- let (sender, receiver) = channel::<()>();
58
- let jsfun = Closure::once_into_js(move || sender.send(()).unwrap());
59
- global::window().add_event_listener_with_callback("load", jsfun.unchecked_ref())?;
60
- Ok(receiver.await?)
58
+ loop {
59
+ let document = global::document();
60
+ let state = document.ready_state();
61
+ if state == "complete" || state == "loaded" {
62
+ return Ok(());
63
+ }
64
+
65
+ if state != "loading" {
66
+ let links = document.query_selector_all("link[rel=stylesheet]")?;
67
+ let applied = (0..links.length()).all(|i| {
68
+ links
69
+ .item(i)
70
+ .and_then(|node| node.dyn_into::<web_sys::HtmlLinkElement>().ok())
71
+ .is_none_or(|link| link.sheet().is_some())
72
+ });
73
+
74
+ if applied {
75
+ return Ok(());
76
+ }
77
+ }
78
+
79
+ set_timeout(50).await?;
61
80
  }
62
81
  }
63
82
 
@@ -523,363 +523,3 @@ pub fn resolve_css_refs(
523
523
 
524
524
  dropped
525
525
  }
526
-
527
- #[cfg(test)]
528
- mod tests {
529
- use serde_json::json;
530
-
531
- use super::*;
532
-
533
- fn stop(color: &str, offset: f64) -> GradientStopSpec {
534
- GradientStopSpec {
535
- color: color.to_owned(),
536
- offset,
537
- }
538
- }
539
-
540
- #[test]
541
- fn colors_normalize_to_lowercase_hex() {
542
- assert_eq!(normalize_css_color("#ABC").unwrap(), "#aabbcc");
543
- assert_eq!(normalize_css_color("#abcd").unwrap(), "#aabbcc");
544
- assert_eq!(normalize_css_color(" #FF7F0E ").unwrap(), "#ff7f0e");
545
- assert_eq!(normalize_css_color("#ff7f0e80").unwrap(), "#ff7f0e");
546
- assert_eq!(normalize_css_color("rgb(255, 127, 14)").unwrap(), "#ff7f0e");
547
- assert_eq!(
548
- normalize_css_color("rgba(255,127,14,0.5)").unwrap(),
549
- "#ff7f0e"
550
- );
551
- assert_eq!(
552
- normalize_css_color("rgb(255 127 14 / 50%)").unwrap(),
553
- "#ff7f0e"
554
- );
555
- assert_eq!(
556
- normalize_css_color("rgb(100%, 0%, 50%)").unwrap(),
557
- "#ff0080"
558
- );
559
- assert_eq!(normalize_css_color("rgb(300, -5, 14)").unwrap(), "#ff000e");
560
- assert!(normalize_css_color("red").is_err());
561
- assert!(normalize_css_color("#12345").is_err());
562
- assert!(normalize_css_color("hsl(0, 100%, 50%)").is_err());
563
- assert!(normalize_css_color("#gggggg").is_err());
564
- assert!(normalize_css_color("").is_err());
565
- }
566
-
567
- #[test]
568
- fn var_refs_are_whole_value_and_namespaced() {
569
- assert_eq!(
570
- parse_var_ref("var(--psp-user--gradient-1)"),
571
- Some("--psp-user--gradient-1")
572
- );
573
- assert_eq!(
574
- parse_var_ref(" var( --psp-user--color-hot ) "),
575
- Some("--psp-user--color-hot")
576
- );
577
- assert_eq!(parse_var_ref("var(--psp-charts--gradient)"), None);
578
- assert_eq!(parse_var_ref("var(--psp-user--)"), None);
579
- assert_eq!(parse_var_ref("var(--psp-user--gradient-1, red)"), None);
580
- assert_eq!(parse_var_ref("#ff0000"), None);
581
- assert_eq!(
582
- parse_var_ref("linear-gradient(var(--psp-user--color-1), #fff)"),
583
- None
584
- );
585
- }
586
-
587
- #[test]
588
- fn kind_is_carried_by_the_reference_prefix() {
589
- assert_eq!(
590
- CssKind::of_var("--psp-user--gradient-1"),
591
- Some(CssKind::Gradient)
592
- );
593
- assert_eq!(
594
- CssKind::of_var("--psp-user--palette-warm"),
595
- Some(CssKind::Palette)
596
- );
597
- assert_eq!(CssKind::of_var("--psp-user--color-1"), Some(CssKind::Color));
598
- assert_eq!(CssKind::of_var("--psp-user--other-1"), None);
599
- assert_eq!(
600
- CssKind::Palette.short_name("--psp-user--palette-warm"),
601
- "warm"
602
- );
603
-
604
- assert_eq!(
605
- CssGradient::parse("var(--psp-user--gradient-1)").unwrap(),
606
- CssGradient::Var("--psp-user--gradient-1".to_owned())
607
- );
608
- assert!(CssGradient::parse("var(--psp-user--palette-1)").is_err());
609
- assert!(CssPalette::parse("var(--psp-user--gradient-1)").is_err());
610
- assert!(CssColor::parse("var(--psp-user--palette-1)").is_err());
611
- assert_eq!(
612
- CssColor::parse("var(--psp-user--color-1)")
613
- .unwrap()
614
- .to_css(),
615
- "var(--psp-user--color-1)"
616
- );
617
-
618
- assert_eq!(
619
- canonicalize_css_palette("var(--psp-user--palette-1)").unwrap(),
620
- "var(--psp-user--palette-1)"
621
- );
622
- }
623
-
624
- #[test]
625
- fn gradient_round_trip_is_canonical_and_idempotent() {
626
- let src = "linear-gradient(to right, #0366d6 0%, #ffffff 33.3%, #ff7f0e 100%)";
627
- let canon = canonicalize_css_gradient(src).unwrap();
628
- assert_eq!(canon, src);
629
- assert_eq!(canonicalize_css_gradient(&canon).unwrap(), canon);
630
-
631
- let CssGradient::Literal(stops) = CssGradient::parse(src).unwrap() else {
632
- panic!("expected literal");
633
- };
634
-
635
- assert_eq!(stops, vec![
636
- stop("#0366d6", 0.0),
637
- stop("#ffffff", 0.333),
638
- stop("#ff7f0e", 1.0),
639
- ]);
640
-
641
- let css = gradient_to_css(&[
642
- stop("#222222", 1.7),
643
- stop("#111111", 1.0 / 3.0),
644
- stop("#000000", -0.5),
645
- ]);
646
-
647
- assert_eq!(
648
- css,
649
- "linear-gradient(to right, #000000 0%, #111111 33.3%, #222222 100%)"
650
- );
651
-
652
- assert_eq!(format_percent(0.375), "37.5%");
653
- assert_eq!(format_percent(0.1), "10%");
654
- assert_eq!(format_percent(1.0), "100%");
655
- }
656
-
657
- #[test]
658
- fn gradient_accepts_loose_input() {
659
- assert_eq!(
660
- canonicalize_css_gradient("linear-gradient(#0366d6 0%, #ff7f0e 100%)").unwrap(),
661
- "linear-gradient(to right, #0366d6 0%, #ff7f0e 100%)"
662
- );
663
-
664
- assert_eq!(
665
- canonicalize_css_gradient("LINEAR-GRADIENT(90deg, RGB(3, 102, 214), #FFF, #ff7f0e)")
666
- .unwrap(),
667
- "linear-gradient(to right, #0366d6 0%, #ffffff 50%, #ff7f0e 100%)"
668
- );
669
-
670
- assert_eq!(
671
- canonicalize_css_gradient(
672
- "linear-gradient(to bottom, #000 10%, #111, #222, #333 70%, #444)"
673
- )
674
- .unwrap(),
675
- "linear-gradient(to right, #000000 10%, #111111 30%, #222222 50%, #333333 70%, \
676
- #444444 100%)"
677
- );
678
-
679
- assert_eq!(
680
- canonicalize_css_gradient("linear-gradient(#fff 120%, #000 -5%)").unwrap(),
681
- "linear-gradient(to right, #000000 0%, #ffffff 100%)"
682
- );
683
- }
684
-
685
- #[test]
686
- fn gradient_rejects_malformed_input() {
687
- assert!(canonicalize_css_gradient("#ff0000").is_err());
688
- assert!(canonicalize_css_gradient("linear-gradient(#ff0000)").is_err());
689
- assert!(canonicalize_css_gradient("linear-gradient(#ff0000 0px, #fff 10px)").is_err());
690
- assert!(canonicalize_css_gradient("linear-gradient(red, blue)").is_err());
691
- assert!(canonicalize_css_gradient("radial-gradient(#000, #fff)").is_err());
692
- assert!(canonicalize_css_gradient("linear-gradient(#000, , #fff)").is_err());
693
- assert!(canonicalize_css_gradient("").is_err());
694
- }
695
-
696
- #[test]
697
- fn palette_round_trip_and_rules() {
698
- let src = "linear-gradient(to right, #0366d6, #ff7f0e, #2ca02c)";
699
- assert_eq!(canonicalize_css_palette(src).unwrap(), src);
700
- assert_eq!(
701
- CssPalette::parse(src).unwrap(),
702
- CssPalette::Literal(vec![
703
- "#0366d6".to_owned(),
704
- "#ff7f0e".to_owned(),
705
- "#2ca02c".to_owned()
706
- ])
707
- );
708
-
709
- assert_eq!(
710
- canonicalize_css_palette("linear-gradient(90deg, rgb(3,102,214), #FFF)").unwrap(),
711
- "linear-gradient(to right, #0366d6, #ffffff)"
712
- );
713
-
714
- assert_eq!(
715
- canonicalize_css_palette("linear-gradient(#abc)").unwrap(),
716
- "linear-gradient(to right, #aabbcc)"
717
- );
718
-
719
- assert_eq!(
720
- palette_to_css(&["#000000".to_owned(), "#ffffff".to_owned()]),
721
- "linear-gradient(to right, #000000, #ffffff)"
722
- );
723
- }
724
-
725
- #[test]
726
- fn palette_rejects_positions() {
727
- let gradient = gradient_to_css(&[stop("#000000", 0.0), stop("#ffffff", 1.0)]);
728
- assert!(CssPalette::parse(&gradient).is_err());
729
- assert!(canonicalize_css_palette("linear-gradient(#000, #fff 50%)").is_err());
730
- assert!(canonicalize_css_palette("linear-gradient()").is_err());
731
- assert!(canonicalize_css_palette("#000000").is_err());
732
-
733
- let palette = palette_to_css(&["#000000".to_owned(), "#ffffff".to_owned()]);
734
- assert_eq!(
735
- canonicalize_css_gradient(&palette).unwrap(),
736
- "linear-gradient(to right, #000000 0%, #ffffff 100%)"
737
- );
738
- }
739
-
740
- #[test]
741
- fn color_canonicalizes_literals_and_passes_refs() {
742
- assert_eq!(canonicalize_css_color("#ABC").unwrap(), "#aabbcc");
743
- assert_eq!(
744
- canonicalize_css_color("var(--psp-user--color-hot)").unwrap(),
745
- "var(--psp-user--color-hot)"
746
- );
747
- assert!(canonicalize_css_color("var(--psp-user--gradient-1)").is_err());
748
- assert!(canonicalize_css_color("linear-gradient(#000, #fff)").is_err());
749
- }
750
-
751
- #[test]
752
- fn palette_set_unions_restored_and_in_use_with_stable_names() {
753
- let mut restored = BTreeMap::new();
754
- restored.insert(
755
- "--psp-user--gradient-heat".to_owned(),
756
- "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
757
- );
758
- restored.insert("--psp-user--color-unused".to_owned(), "#123456".to_owned());
759
- let host = vec![NamedValue {
760
- name: "--psp-user--palette-1".to_owned(),
761
- value: "linear-gradient(to right, #ff0000, #00ff00)".to_owned(),
762
- }];
763
- let taken =
764
- |name: &str| name == "--psp-user--gradient-1" || name == "--psp-user--palette-1";
765
- let in_use = vec![
766
- (
767
- CssKind::Gradient,
768
- "linear-gradient(to right, #000000 0%, #ffffff 100%)".to_owned(),
769
- ),
770
- (
771
- CssKind::Gradient,
772
- "linear-gradient(to right, #111111 0%, #222222 100%)".to_owned(),
773
- ),
774
- (
775
- CssKind::Gradient,
776
- "linear-gradient(to right, #111111 0%, #222222 100%)".to_owned(),
777
- ),
778
- (
779
- CssKind::Palette,
780
- "linear-gradient(to right, #ff0000, #00ff00)".to_owned(),
781
- ),
782
- (CssKind::Color, "#abcdef".to_owned()),
783
- ];
784
-
785
- let set = assign_palette_names(&restored, &host, &in_use, &taken);
786
- assert_eq!(
787
- set.iter()
788
- .map(|(k, v)| (k.as_str(), v.as_str()))
789
- .collect::<Vec<_>>(),
790
- vec![
791
- ("--psp-user--color-1", "#abcdef"),
792
- ("--psp-user--color-unused", "#123456"),
793
- (
794
- "--psp-user--gradient-2",
795
- "linear-gradient(to right, #111111 0%, #222222 100%)"
796
- ),
797
- (
798
- "--psp-user--gradient-heat",
799
- "linear-gradient(to right, #000000 0%, #ffffff 100%)"
800
- ),
801
- (
802
- "--psp-user--palette-1",
803
- "linear-gradient(to right, #ff0000, #00ff00)"
804
- ),
805
- ]
806
- );
807
-
808
- assert_eq!(
809
- palette_name_for(
810
- &set,
811
- CssKind::Gradient,
812
- "linear-gradient(to right, #111111 0%, #222222 100%)"
813
- )
814
- .as_deref(),
815
- Some("--psp-user--gradient-2")
816
- );
817
- assert_eq!(palette_name_for(&set, CssKind::Gradient, "#abcdef"), None);
818
-
819
- assert_eq!(assign_palette_names(&set, &host, &in_use, &taken), set);
820
- }
821
-
822
- #[test]
823
- fn resolver_inlines_known_refs_and_drops_the_rest() {
824
- let host = |name: &str| -> Option<String> {
825
- match name {
826
- "--psp-user--gradient-1" => Some("linear-gradient(#000, #fff)".to_owned()),
827
- "--psp-user--palette-warm" => Some("linear-gradient(#f00, #ff0)".to_owned()),
828
- "--psp-user--color-hot" => Some("rgb(255, 0, 0)".to_owned()),
829
- "--psp-user--palette-bad" => Some("linear-gradient(#000 0%, #fff 100%)".to_owned()),
830
- "--psp-user--color-empty" => Some(" ".to_owned()),
831
- _ => None,
832
- }
833
- };
834
-
835
- let mut entry = json!({
836
- "gradient": "var(--psp-user--gradient-1)",
837
- "palette": "var(--psp-user--palette-warm)",
838
- "color": "var(--psp-user--color-hot)",
839
- "bad_palette": "var(--psp-user--palette-bad)",
840
- "missing": "var(--psp-user--gradient-missing)",
841
- "empty": "var(--psp-user--color-empty)",
842
- "unknown_kind": "var(--psp-user--other-1)",
843
- "literal": "#123456",
844
- "number": 3,
845
- })
846
- .as_object()
847
- .unwrap()
848
- .clone();
849
-
850
- let mut dropped = resolve_css_refs(&mut entry, &host);
851
- dropped.sort();
852
- assert_eq!(dropped, vec![
853
- (
854
- "bad_palette".to_owned(),
855
- "--psp-user--palette-bad".to_owned()
856
- ),
857
- ("empty".to_owned(), "--psp-user--color-empty".to_owned()),
858
- (
859
- "missing".to_owned(),
860
- "--psp-user--gradient-missing".to_owned()
861
- ),
862
- ("unknown_kind".to_owned(), "--psp-user--other-1".to_owned()),
863
- ]);
864
-
865
- assert_eq!(
866
- entry,
867
- json!({
868
- "gradient": "linear-gradient(to right, #000000 0%, #ffffff 100%)",
869
- "palette": "linear-gradient(to right, #ff0000, #ffff00)",
870
- "color": "#ff0000",
871
- "literal": "#123456",
872
- "number": 3,
873
- })
874
- .as_object()
875
- .unwrap()
876
- .clone()
877
- );
878
-
879
- assert!(entry.values().all(|v| {
880
- v.as_str()
881
- .map(|s| parse_var_ref(s).is_none())
882
- .unwrap_or(true)
883
- }));
884
- }
885
- }
@@ -357,39 +357,3 @@ fn measure_width(ctx: &web_sys::CanvasRenderingContext2d, family: &str) -> f64 {
357
357
  .map(|x| x.width())
358
358
  .unwrap_or_default()
359
359
  }
360
-
361
- #[cfg(test)]
362
- mod tests {
363
- use super::*;
364
-
365
- fn owned(xs: &[&str]) -> Vec<String> {
366
- xs.iter().map(|x| (*x).to_owned()).collect()
367
- }
368
-
369
- #[test]
370
- fn generic_families_lead_and_current_appends_when_missing() {
371
- let options = compose_font_family_options("Zapf Chancery", &owned(&["Arial", "Menlo"]));
372
- assert_eq!(
373
- options,
374
- owned(&[
375
- "inherit",
376
- "monospace",
377
- "sans-serif",
378
- "serif",
379
- "system-ui",
380
- "Arial",
381
- "Menlo",
382
- "Zapf Chancery"
383
- ])
384
- );
385
- }
386
-
387
- #[test]
388
- fn current_is_not_duplicated() {
389
- let options = compose_font_family_options("Arial", &owned(&["Arial"]));
390
- assert_eq!(options.iter().filter(|x| *x == "Arial").count(), 1);
391
-
392
- let options = compose_font_family_options("inherit", &[]);
393
- assert_eq!(options, owned(&GENERIC_FONT_FAMILIES));
394
- }
395
- }
@@ -29,14 +29,12 @@ mod local_fonts;
29
29
  mod modal_position;
30
30
  mod number_format;
31
31
  mod pending_effects;
32
+ mod plugin_host_state;
32
33
  mod ptr_eq_rc;
33
34
  mod pubsub;
34
35
  mod spawn;
35
36
  mod weak_scope;
36
37
 
37
- #[cfg(test)]
38
- mod tests;
39
-
40
38
  pub use browser::*;
41
39
  pub use completion::*;
42
40
  pub use css::*;
@@ -51,6 +49,7 @@ pub use modal_position::*;
51
49
  pub use number_format::*;
52
50
  pub use pending_effects::*;
53
51
  pub use perspective_client::clone;
52
+ pub use plugin_host_state::*;
54
53
  pub use ptr_eq_rc::*;
55
54
  pub use pubsub::*;
56
55
  pub use spawn::*;
@@ -39,36 +39,30 @@ impl ModalAnchor {
39
39
  }
40
40
  }
41
41
 
42
- /// Given the bounds of the target element as previously computed, as well as
43
- /// the browser's viewport and the bounds of the already-connected modal element
44
- /// itself, determine the best anchor point to keep the element on-screen.
45
- pub fn calc_relative_position(
46
- elem: &HtmlElement,
47
- _top: f64,
48
- left: f64,
49
- height: f64,
50
- width: f64,
51
- ) -> ModalAnchor {
42
+ /// Pick the anchor that keeps a modal of `modal`'s size on-screen when opened
43
+ /// against `target`, from those rects alone so repeated calls agree.
44
+ pub fn calc_relative_position(target: &DomRect, modal: &DomRect) -> ModalAnchor {
52
45
  let window = global::window();
53
- let rect = elem.get_bounding_client_rect();
54
46
  let inner_width = window.inner_width().unwrap().as_f64().unwrap();
55
47
  let inner_height = window.inner_height().unwrap().as_f64().unwrap();
56
- let rect_top = rect.top();
57
- let rect_height = rect.height();
58
- let rect_width = rect.width();
59
- let rect_left = rect.left();
48
+ let top = target.top();
49
+ let left = target.left();
50
+ let height = target.height();
51
+ let width = target.width();
52
+ let modal_height = modal.height();
53
+ let modal_width = modal.width();
60
54
 
61
- let elem_over_y = inner_height < rect_top + rect_height;
62
- let elem_over_x = inner_width < rect_left + rect_width;
63
- let target_over_x = inner_width < rect_left + width;
64
- let target_over_y = inner_height < rect_top + height;
55
+ let elem_over_y = inner_height < top + height - 1.0 + modal_height;
56
+ let elem_over_x = inner_width < left + modal_width;
57
+ let target_over_x = inner_width < left + width;
58
+ let target_over_y = inner_height < top + height;
65
59
 
66
60
  // modal/target
67
61
  match (elem_over_y, elem_over_x, target_over_x, target_over_y) {
68
62
  (true, _, true, true) => ModalAnchor::BottomRightTopLeft,
69
63
  (true, _, true, false) => ModalAnchor::BottomRightBottomLeft,
70
64
  (true, true, false, _) => {
71
- if left + width - rect_width > 0.0 {
65
+ if left + width - modal_width > 0.0 {
72
66
  ModalAnchor::BottomRightTopRight
73
67
  } else {
74
68
  ModalAnchor::BottomLeftTopLeft
@@ -77,7 +71,7 @@ pub fn calc_relative_position(
77
71
  (true, false, false, _) => ModalAnchor::BottomLeftTopLeft,
78
72
  (false, true, true, _) => ModalAnchor::TopRightTopLeft,
79
73
  (false, true, false, _) => {
80
- if left + width - rect_width > 0.0 {
74
+ if left + width - modal_width > 0.0 {
81
75
  ModalAnchor::TopRightBottomRight
82
76
  } else {
83
77
  ModalAnchor::TopLeftBottomLeft
@@ -10,4 +10,34 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- mod debounce;
13
+ use web_sys::Element;
14
+
15
+ /// Mirror the host `<perspective-viewer>`'s `settings` attribute and
16
+ /// `shift-active` class onto one slotted plugin element so plugin stylesheets
17
+ /// can match them with `:host(...)`.
18
+ pub fn stamp_plugin_host_state(viewer: &Element, plugin: &Element) {
19
+ let _ = plugin.toggle_attribute_with_force("settings", viewer.has_attribute("settings"));
20
+ let _ = plugin
21
+ .class_list()
22
+ .toggle_with_force("shift-active", viewer.class_list().contains("shift-active"));
23
+ }
24
+
25
+ /// Apply [`stamp_plugin_host_state`] to every plugin element slotted into
26
+ /// `viewer`.
27
+ pub fn stamp_plugin_host_state_all(viewer: &Element) {
28
+ let children = viewer.children();
29
+ for i in 0..children.length() {
30
+ let Some(el) = children.item(i) else {
31
+ continue;
32
+ };
33
+
34
+ let is_plugin = el.tag_name().starts_with("PERSPECTIVE-VIEWER-")
35
+ && el
36
+ .get_attribute("slot")
37
+ .is_some_and(|slot| !slot.starts_with("tab-"));
38
+
39
+ if is_plugin {
40
+ stamp_plugin_host_state(viewer, &el);
41
+ }
42
+ }
43
+ }
@@ -127,7 +127,6 @@ impl<T: Clone + 'static> PubSub<T> {
127
127
  }
128
128
 
129
129
  /// Convert [`PubSub::emit`] to a `Box<dyn Fn(T)>`.
130
- #[must_use]
131
130
  pub fn as_boxfn(&self) -> Box<dyn Fn(T) + 'static> {
132
131
  let internal = PubSub(self.0.clone());
133
132
  Box::new(move |val: T| internal.emit(val))