@perspective-dev/viewer 5.1.0 → 5.3.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 (245) hide show
  1. package/dist/cdn/perspective-viewer.js +9 -9
  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/intl/de.css +1 -1
  9. package/dist/css/intl/es.css +1 -1
  10. package/dist/css/intl/fr.css +1 -1
  11. package/dist/css/intl/ja.css +1 -1
  12. package/dist/css/intl/pt.css +1 -1
  13. package/dist/css/intl/zh.css +1 -1
  14. package/dist/css/intl.css +1 -1
  15. package/dist/css/monokai.css +1 -1
  16. package/dist/css/phosphor.css +1 -1
  17. package/dist/css/pro-dark.css +1 -1
  18. package/dist/css/pro.css +1 -1
  19. package/dist/css/solarized-dark.css +1 -1
  20. package/dist/css/solarized.css +1 -1
  21. package/dist/css/themes.css +1 -1
  22. package/dist/css/vaporwave.css +1 -1
  23. package/dist/docs/perspective-docs.json +1 -0
  24. package/dist/esm/column-format.d.ts +19 -28
  25. package/dist/esm/perspective-viewer.d.ts +3 -0
  26. package/dist/esm/perspective-viewer.inline.js +9 -9
  27. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  28. package/dist/esm/perspective-viewer.js +9 -9
  29. package/dist/esm/perspective-viewer.js.map +4 -4
  30. package/dist/esm/plugin.d.ts +10 -0
  31. package/dist/esm/providers.d.ts +79 -0
  32. package/dist/esm/ts-rs/CompactDisplay.d.ts +5 -0
  33. package/dist/esm/ts-rs/CurrencyCode.d.ts +1 -0
  34. package/dist/esm/ts-rs/CurrencyDisplay.d.ts +1 -0
  35. package/dist/esm/ts-rs/CurrencyNumberFormatStyle.d.ts +8 -0
  36. package/dist/esm/ts-rs/CurrencySign.d.ts +1 -0
  37. package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +1 -0
  38. package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +54 -0
  39. package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +34 -0
  40. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +1 -0
  41. package/dist/esm/ts-rs/DatetimeFormatType.d.ts +6 -0
  42. package/dist/esm/ts-rs/FormatMode.d.ts +1 -0
  43. package/dist/esm/ts-rs/FormatUnit.d.ts +1 -0
  44. package/dist/esm/ts-rs/Notation.d.ts +17 -0
  45. package/dist/esm/ts-rs/NumberFormatStyle.d.ts +18 -0
  46. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +42 -2
  47. package/dist/esm/ts-rs/PluginStaticConfig.d.ts +35 -0
  48. package/dist/esm/ts-rs/RestoreOptions.d.ts +19 -0
  49. package/dist/esm/ts-rs/RoundingMode.d.ts +1 -0
  50. package/dist/esm/ts-rs/RoundingPriority.d.ts +1 -0
  51. package/dist/esm/ts-rs/SaveWorkspaceOptions.d.ts +10 -0
  52. package/dist/esm/ts-rs/SignDisplay.d.ts +1 -0
  53. package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +1 -0
  54. package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +24 -0
  55. package/dist/esm/ts-rs/SplitRollupMode.d.ts +9 -0
  56. package/dist/esm/ts-rs/StringColorMode.d.ts +1 -0
  57. package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +1 -0
  58. package/dist/esm/ts-rs/Unit.d.ts +1 -0
  59. package/dist/esm/ts-rs/UnitDisplay.d.ts +1 -0
  60. package/dist/esm/ts-rs/UnitNumberFormatStyle.d.ts +6 -0
  61. package/dist/esm/ts-rs/UseGrouping.d.ts +1 -0
  62. package/dist/esm/ts-rs/ViewerConfig.d.ts +42 -2
  63. package/dist/esm/ts-rs/ViewerConfigInitial.d.ts +175 -0
  64. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +63 -0
  65. package/dist/esm/ts-rs/WindowSpec.d.ts +3 -2
  66. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  67. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +20 -9
  68. package/dist/wasm/perspective-viewer.d.ts +157 -41
  69. package/dist/wasm/perspective-viewer.js +452 -151
  70. package/dist/wasm/perspective-viewer.wasm +0 -0
  71. package/dist/wasm/perspective-viewer.wasm.d.ts +22 -17
  72. package/package.json +2 -1
  73. package/src/css/_viewer_bundle.css +1 -0
  74. package/src/css/column-selector.css +8 -0
  75. package/src/css/column-settings-panel.css +9 -5
  76. package/src/css/column-style.css +209 -31
  77. package/src/css/config-selector.css +38 -3
  78. package/src/css/dom/select.css +2 -1
  79. package/src/css/form/code-editor.css +6 -7
  80. package/src/css/form/mirrored-textarea.css +81 -0
  81. package/src/css/viewer.css +328 -0
  82. package/src/rust/agent/client/mod.rs +19 -0
  83. package/src/rust/agent/client/protocol.rs +320 -0
  84. package/src/rust/agent/client/transport.rs +287 -0
  85. package/src/rust/agent/client/turn.rs +174 -0
  86. package/src/rust/agent/config.rs +159 -0
  87. package/src/rust/agent/docs.rs +293 -0
  88. package/src/rust/agent/markdown/block.rs +509 -0
  89. package/src/rust/agent/markdown/entity.rs +52 -0
  90. package/src/rust/agent/markdown/events.rs +71 -0
  91. package/src/rust/agent/markdown/inline.rs +497 -0
  92. package/src/rust/agent/markdown/mod.rs +466 -0
  93. package/src/rust/agent/mod.rs +23 -0
  94. package/src/rust/agent/preamble.md +16 -0
  95. package/src/rust/agent/runtime.rs +201 -0
  96. package/src/rust/agent/slot.rs +236 -0
  97. package/src/rust/agent/tools.rs +1141 -0
  98. package/src/rust/components/chat_panel.rs +289 -0
  99. package/src/rust/components/column_selector/active_column.rs +2 -2
  100. package/src/rust/components/column_selector/add_expression_button.rs +3 -3
  101. package/src/rust/components/column_selector/aggregate_selector.rs +2 -3
  102. package/src/rust/components/column_selector/config_selector.rs +84 -7
  103. package/src/rust/components/column_selector/expr_edit_button.rs +4 -10
  104. package/src/rust/components/column_selector/inactive_column.rs +2 -2
  105. package/src/rust/components/column_selector.rs +55 -4
  106. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +293 -126
  107. package/src/rust/components/column_settings_sidebar/style_tab.rs +262 -225
  108. package/src/rust/components/column_settings_sidebar.rs +88 -69
  109. package/src/rust/components/containers/dragdrop_list.rs +37 -0
  110. package/src/rust/components/editable_header.rs +85 -144
  111. package/src/rust/components/expression_editor.rs +9 -16
  112. package/src/rust/components/form/code_editor.rs +29 -28
  113. package/src/rust/components/form/debug.rs +9 -5
  114. package/src/rust/components/form/mirrored_textarea.rs +165 -0
  115. package/src/rust/components/form/mod.rs +4 -1
  116. package/src/rust/components/form/multi_stop_gradient_selector.rs +476 -0
  117. package/src/rust/components/form/named_value_picker.rs +86 -0
  118. package/src/rust/components/form/palette_selector.rs +440 -0
  119. package/src/rust/components/main_panel/msg.rs +2 -2
  120. package/src/rust/components/main_panel/reconcile.rs +4 -4
  121. package/src/rust/components/mod.rs +2 -0
  122. package/src/rust/components/plugin_tab.rs +4 -28
  123. package/src/rust/components/settings_panel.rs +105 -22
  124. package/src/rust/components/viewer/msg.rs +46 -7
  125. package/src/rust/components/viewer/panels.rs +17 -20
  126. package/src/rust/components/viewer/render.rs +10 -3
  127. package/src/rust/components/viewer/settings.rs +163 -23
  128. package/src/rust/components/viewer/snapshots.rs +46 -21
  129. package/src/rust/components/viewer/wiring.rs +14 -25
  130. package/src/rust/components/viewer.rs +12 -8
  131. package/src/rust/components/window_editor.rs +75 -103
  132. package/src/rust/config/column_config_schema.rs +100 -41
  133. package/src/rust/config/css_scale.rs +882 -0
  134. package/src/rust/config/datetime_column_style/custom.rs +17 -0
  135. package/src/rust/config/datetime_column_style/simple.rs +10 -0
  136. package/src/rust/config/mod.rs +2 -0
  137. package/src/rust/config/number_string_format.rs +33 -0
  138. package/src/rust/config/options.rs +28 -0
  139. package/src/rust/config/plugin_static_config.rs +120 -5
  140. package/src/rust/config/viewer_config.rs +216 -3
  141. package/src/rust/config/workspace_config.rs +138 -10
  142. package/src/rust/custom_elements/viewer.rs +369 -119
  143. package/src/rust/custom_events.rs +43 -1
  144. package/src/rust/js/regular_layout.rs +3 -3
  145. package/src/rust/lib.rs +15 -0
  146. package/src/rust/presentation/column_locator.rs +19 -5
  147. package/src/rust/presentation.rs +200 -36
  148. package/src/rust/queries/column_locator.rs +74 -44
  149. package/src/rust/queries/get_viewer_config.rs +2 -1
  150. package/src/rust/queries/mod.rs +2 -0
  151. package/src/rust/queries/palette_set.rs +82 -0
  152. package/src/rust/queries/plugin_column_styles.rs +6 -2
  153. package/src/rust/renderer/dispatch.rs +16 -4
  154. package/src/rust/renderer/plugin_config.rs +309 -47
  155. package/src/rust/renderer.rs +15 -42
  156. package/src/rust/session/column_defaults_update.rs +32 -7
  157. package/src/rust/session/metadata.rs +23 -21
  158. package/src/rust/session/props.rs +1 -0
  159. package/src/rust/session/replace_expression_update.rs +1 -0
  160. package/src/rust/session.rs +9 -6
  161. package/src/rust/tasks/create_panel.rs +46 -49
  162. package/src/rust/tasks/edit_expression.rs +19 -23
  163. package/src/rust/tasks/edit_window.rs +9 -11
  164. package/src/rust/tasks/eject.rs +4 -1
  165. package/src/rust/tasks/pipeline.rs +2 -2
  166. package/src/rust/tasks/presize_panels.rs +44 -5
  167. package/src/rust/tasks/reset_all.rs +4 -4
  168. package/src/rust/tasks/restore_and_render.rs +17 -44
  169. package/src/rust/tasks/restore_panel.rs +37 -22
  170. package/src/rust/tasks/set_edit_mode.rs +3 -1
  171. package/src/rust/tasks/sync_update_panels.rs +18 -24
  172. package/src/rust/tasks/update_theme.rs +23 -18
  173. package/src/rust/utils/css_vars.rs +26 -0
  174. package/src/rust/utils/mod.rs +2 -0
  175. package/src/rust/utils/spawn.rs +1 -1
  176. package/src/rust/workspace.rs +454 -199
  177. package/src/svg/lock.svg +7 -0
  178. package/src/themes/botanical.css +10 -0
  179. package/src/themes/defaults.css +12 -0
  180. package/src/themes/dracula.css +17 -0
  181. package/src/themes/gruvbox-dark.css +20 -0
  182. package/src/themes/gruvbox.css +10 -0
  183. package/src/themes/icons.css +1 -0
  184. package/src/themes/intl/de.css +16 -0
  185. package/src/themes/intl/es.css +16 -0
  186. package/src/themes/intl/fr.css +16 -0
  187. package/src/themes/intl/ja.css +16 -0
  188. package/src/themes/intl/pt.css +16 -0
  189. package/src/themes/intl/zh.css +16 -0
  190. package/src/themes/intl.css +16 -0
  191. package/src/themes/monokai.css +22 -0
  192. package/src/themes/phosphor.css +5 -0
  193. package/src/themes/pro-dark.css +10 -0
  194. package/src/themes/solarized-dark.css +7 -0
  195. package/src/themes/solarized.css +7 -0
  196. package/src/themes/vaporwave.css +20 -0
  197. package/src/ts/column-format.ts +67 -46
  198. package/src/ts/perspective-viewer.ts +3 -0
  199. package/src/ts/plugin.ts +14 -0
  200. package/src/ts/providers.ts +96 -0
  201. package/src/ts/ts-rs/CompactDisplay.ts +3 -0
  202. package/src/ts/ts-rs/CurrencyCode.ts +3 -0
  203. package/src/ts/ts-rs/CurrencyDisplay.ts +3 -0
  204. package/src/ts/ts-rs/CurrencyNumberFormatStyle.ts +6 -0
  205. package/src/ts/ts-rs/CurrencySign.ts +3 -0
  206. package/src/ts/ts-rs/CustomDatetimeFormat.ts +3 -0
  207. package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +54 -0
  208. package/src/ts/ts-rs/CustomNumberFormatConfig.ts +25 -0
  209. package/src/ts/ts-rs/DatetimeColorMode.ts +3 -0
  210. package/src/ts/ts-rs/DatetimeFormatType.ts +8 -0
  211. package/src/ts/ts-rs/FormatMode.ts +3 -0
  212. package/src/ts/ts-rs/FormatUnit.ts +3 -0
  213. package/src/ts/ts-rs/Notation.ts +11 -0
  214. package/src/ts/ts-rs/NumberFormatStyle.ts +12 -0
  215. package/src/ts/ts-rs/PanelViewerConfig.ts +48 -2
  216. package/src/ts/ts-rs/PluginStaticConfig.ts +36 -1
  217. package/src/ts/ts-rs/RestoreOptions.ts +20 -0
  218. package/src/ts/ts-rs/RoundingMode.ts +3 -0
  219. package/src/ts/ts-rs/RoundingPriority.ts +3 -0
  220. package/src/ts/ts-rs/SaveWorkspaceOptions.ts +11 -0
  221. package/src/ts/ts-rs/SignDisplay.ts +3 -0
  222. package/src/ts/ts-rs/SimpleDatetimeFormat.ts +3 -0
  223. package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +25 -0
  224. package/src/ts/ts-rs/SplitRollupMode.ts +11 -0
  225. package/src/ts/ts-rs/StringColorMode.ts +3 -0
  226. package/src/ts/ts-rs/TrailingZeroDisplay.ts +3 -0
  227. package/src/ts/ts-rs/Unit.ts +3 -0
  228. package/src/ts/ts-rs/UnitDisplay.ts +3 -0
  229. package/src/ts/ts-rs/UnitNumberFormatStyle.ts +5 -0
  230. package/src/ts/ts-rs/UseGrouping.ts +3 -0
  231. package/src/ts/ts-rs/ViewerConfig.ts +48 -2
  232. package/src/ts/ts-rs/ViewerConfigInitial.ts +164 -0
  233. package/src/ts/ts-rs/ViewerConfigUpdate.ts +72 -2
  234. package/src/ts/ts-rs/WindowSpec.ts +3 -2
  235. package/src/ts/ts-rs/WorkspaceConfig.ts +8 -2
  236. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +18 -9
  237. package/dist/esm/ts-rs/WindowAggregate.d.ts +0 -1
  238. package/src/rust/components/form/color_range_selector.rs +0 -137
  239. package/src/ts/ts-rs/WindowAggregate.ts +0 -3
  240. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-8a8c7d5d696b43ae → perspective-viewer-39c3c3b28e3cbd48}/inline5.js +0 -0
@@ -0,0 +1,476 @@
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 itertools::Itertools;
14
+ use wasm_bindgen::JsCast;
15
+ use web_sys::*;
16
+ use yew::prelude::*;
17
+
18
+ use crate::config::{GradientStopSpec, canonicalize_gradient_stops};
19
+
20
+ /// The widget for [`crate::config::ControlSpec::GradientStops`]: a
21
+ /// horizontal gradient preview bar with one handle per stop.
22
+ #[derive(Properties, PartialEq)]
23
+ pub struct MultiStopGradientProps {
24
+ pub stops: Vec<GradientStopSpec>,
25
+ pub on_change: Callback<Vec<GradientStopSpec>>,
26
+ pub on_reset: Callback<()>,
27
+ pub is_modified: bool,
28
+
29
+ #[prop_or_default]
30
+ pub discrete: bool,
31
+
32
+ #[prop_or_default]
33
+ pub title: Option<String>,
34
+ }
35
+
36
+ fn event_index(target: &Element) -> Option<usize> {
37
+ target
38
+ .get_attribute("data-index")
39
+ .and_then(|x| x.parse::<usize>().ok())
40
+ }
41
+
42
+ fn parse_hex_color(color: &str) -> Option<(f64, f64, f64)> {
43
+ let hex = color.strip_prefix('#')?;
44
+ if hex.len() != 6 {
45
+ return None;
46
+ }
47
+
48
+ let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
49
+ let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
50
+ let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
51
+ Some((r as f64, g as f64, b as f64))
52
+ }
53
+
54
+ pub(crate) fn sample_gradient_hex(stops: &[GradientStopSpec], t: f64) -> String {
55
+ let Some(first) = stops.first() else {
56
+ return "#000000".to_owned();
57
+ };
58
+
59
+ if t <= first.offset {
60
+ return first.color.clone();
61
+ }
62
+
63
+ let last = stops.last().unwrap();
64
+ if t >= last.offset {
65
+ return last.color.clone();
66
+ }
67
+
68
+ for pair in stops.windows(2) {
69
+ let (a, b) = (&pair[0], &pair[1]);
70
+ if t > b.offset {
71
+ continue;
72
+ }
73
+
74
+ let span = b.offset - a.offset;
75
+ let u = if span > 0.0 {
76
+ (t - a.offset) / span
77
+ } else {
78
+ 0.0
79
+ };
80
+ let nearer = if u < 0.5 { a } else { b };
81
+ let (Some((ar, ag, ab)), Some((br, bg, bb))) =
82
+ (parse_hex_color(&a.color), parse_hex_color(&b.color))
83
+ else {
84
+ return nearer.color.clone();
85
+ };
86
+
87
+ let scale = |x: f64, y: f64| ((x + (y - x) * u).round().clamp(0.0, 255.0)) as u8;
88
+ return format!(
89
+ "#{:02x}{:02x}{:02x}",
90
+ scale(ar, br),
91
+ scale(ag, bg),
92
+ scale(ab, bb)
93
+ );
94
+ }
95
+
96
+ last.color.clone()
97
+ }
98
+
99
+ const EDGE_PAD_PX: f64 = 8.0;
100
+
101
+ fn track_position(offset: f64) -> String {
102
+ format!(
103
+ "calc({EDGE_PAD_PX}px + (100% - {:.0}px) * {offset:.4})",
104
+ EDGE_PAD_PX * 2.0
105
+ )
106
+ }
107
+
108
+ fn css_gradient(stops: &[GradientStopSpec]) -> String {
109
+ let body = stops
110
+ .iter()
111
+ .sorted_by(|a, b| {
112
+ a.offset
113
+ .partial_cmp(&b.offset)
114
+ .unwrap_or(std::cmp::Ordering::Equal)
115
+ })
116
+ .map(|stop| format!("{} {}", stop.color, track_position(stop.offset)))
117
+ .join(", ");
118
+
119
+ format!("background:linear-gradient(to right, {body})")
120
+ }
121
+
122
+ fn equalized_offset(stops: &[GradientStopSpec], index: usize) -> f64 {
123
+ let left = if index == 0 {
124
+ 0.0
125
+ } else {
126
+ stops[index - 1].offset
127
+ };
128
+
129
+ let right = if index + 1 < stops.len() {
130
+ stops[index + 1].offset
131
+ } else {
132
+ 1.0
133
+ };
134
+
135
+ (left + right) / 2.0
136
+ }
137
+
138
+ fn offset_from_pointer(bar: &Option<Element>, client_x: i32) -> Option<f64> {
139
+ let rect = bar.as_ref()?.get_bounding_client_rect();
140
+ let track_width = rect.width() - 2.0 * EDGE_PAD_PX;
141
+ if track_width <= 0.0 {
142
+ return None;
143
+ }
144
+
145
+ Some(((client_x as f64 - rect.left() - EDGE_PAD_PX) / track_width).clamp(0.0, 1.0))
146
+ }
147
+
148
+ #[function_component(MultiStopGradientSelector)]
149
+ pub fn multi_stop_gradient_selector(props: &MultiStopGradientProps) -> Html {
150
+ let bar_ref = use_node_ref();
151
+
152
+ let drag = use_state(|| Option::<(usize, f64)>::None);
153
+
154
+ let display = {
155
+ let mut stops = props.stops.clone();
156
+ if let Some((index, offset)) = *drag
157
+ && let Some(stop) = stops.get_mut(index)
158
+ {
159
+ stop.offset = offset;
160
+ }
161
+
162
+ stops
163
+ };
164
+
165
+ let on_color = use_callback(
166
+ (props.stops.clone(), props.on_change.clone()),
167
+ |event: InputEvent, (stops, on_change)| {
168
+ let input = event.target().unwrap().unchecked_into::<HtmlInputElement>();
169
+
170
+ let Some(index) = event_index(&input) else {
171
+ return;
172
+ };
173
+
174
+ let mut next = stops.clone();
175
+ let Some(stop) = next.get_mut(index) else {
176
+ return;
177
+ };
178
+
179
+ stop.color = input.value();
180
+ on_change.emit(canonicalize_gradient_stops(next));
181
+ },
182
+ );
183
+
184
+ let on_pointerdown = {
185
+ let drag = drag.clone();
186
+ let stops = props.stops.clone();
187
+ Callback::from(move |event: PointerEvent| {
188
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
189
+ return;
190
+ };
191
+
192
+ let Some(index) = event_index(&target) else {
193
+ return;
194
+ };
195
+
196
+ let Some(stop) = stops.get(index) else {
197
+ return;
198
+ };
199
+
200
+ let _ = target.set_pointer_capture(event.pointer_id());
201
+ drag.set(Some((index, stop.offset)));
202
+ })
203
+ };
204
+
205
+ let on_pointermove = {
206
+ let drag = drag.clone();
207
+ let bar_ref = bar_ref.clone();
208
+ Callback::from(move |event: PointerEvent| {
209
+ let Some((index, _)) = *drag else {
210
+ return;
211
+ };
212
+
213
+ let Some(offset) = offset_from_pointer(&bar_ref.cast::<Element>(), event.client_x())
214
+ else {
215
+ return;
216
+ };
217
+
218
+ drag.set(Some((index, offset)));
219
+ })
220
+ };
221
+
222
+ let on_pointerup = {
223
+ let drag = drag.clone();
224
+ let bar_ref = bar_ref.clone();
225
+ let stops = props.stops.clone();
226
+ let on_change = props.on_change.clone();
227
+ Callback::from(move |event: PointerEvent| {
228
+ let Some((index, offset)) = *drag else {
229
+ return;
230
+ };
231
+
232
+ let offset =
233
+ offset_from_pointer(&bar_ref.cast::<Element>(), event.client_x()).unwrap_or(offset);
234
+
235
+ drag.set(None);
236
+
237
+ let unchanged = stops
238
+ .get(index)
239
+ .map(|stop| (stop.offset * 1000.0).round() == (offset * 1000.0).round())
240
+ .unwrap_or(true);
241
+
242
+ if unchanged {
243
+ return;
244
+ }
245
+
246
+ let mut next = stops.clone();
247
+ if let Some(stop) = next.get_mut(index) {
248
+ stop.offset = offset;
249
+ }
250
+
251
+ on_change.emit(canonicalize_gradient_stops(next));
252
+ })
253
+ };
254
+
255
+ let on_grip_dblclick = use_callback(
256
+ (props.stops.clone(), props.on_change.clone()),
257
+ |event: MouseEvent, (stops, on_change)| {
258
+ event.stop_propagation();
259
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
260
+ return;
261
+ };
262
+
263
+ let Some(index) = event_index(&target) else {
264
+ return;
265
+ };
266
+
267
+ if index >= stops.len() {
268
+ return;
269
+ }
270
+
271
+ let mut next = stops.clone();
272
+ next[index].offset = equalized_offset(stops, index);
273
+ on_change.emit(canonicalize_gradient_stops(next));
274
+ },
275
+ );
276
+
277
+ let on_remove = use_callback(
278
+ (props.stops.clone(), props.on_change.clone()),
279
+ |event: MouseEvent, (stops, on_change)| {
280
+ let Some(target) = event.target().and_then(|x| x.dyn_into::<Element>().ok()) else {
281
+ return;
282
+ };
283
+
284
+ let Some(index) = event_index(&target) else {
285
+ return;
286
+ };
287
+
288
+ if stops.len() > 2 && index < stops.len() {
289
+ let mut next = stops.clone();
290
+ next.remove(index);
291
+ on_change.emit(canonicalize_gradient_stops(next));
292
+ }
293
+ },
294
+ );
295
+
296
+ let can_add = !props.discrete;
297
+
298
+ let on_dblclick = {
299
+ let bar_ref = bar_ref.clone();
300
+ let stops = props.stops.clone();
301
+ let on_change = props.on_change.clone();
302
+ Callback::from(move |event: MouseEvent| {
303
+ if !can_add {
304
+ return;
305
+ }
306
+
307
+ let Some(bar) = bar_ref.cast::<Element>() else {
308
+ return;
309
+ };
310
+
311
+ if event.target() != Some(bar.clone().unchecked_into()) {
312
+ return;
313
+ }
314
+
315
+ let Some(offset) = offset_from_pointer(&Some(bar), event.client_x()) else {
316
+ return;
317
+ };
318
+
319
+ let mut next = stops.clone();
320
+ next.push(GradientStopSpec {
321
+ color: sample_gradient_hex(&next, offset),
322
+ offset,
323
+ });
324
+
325
+ on_change.emit(canonicalize_gradient_stops(next));
326
+ })
327
+ };
328
+
329
+ let on_reset = use_callback(props.on_reset.clone(), |_: MouseEvent, on_reset| {
330
+ on_reset.emit(())
331
+ });
332
+
333
+ let can_remove = props.stops.len() > 2;
334
+ let can_drag = !props.discrete;
335
+ let pinned = props.discrete && !can_remove;
336
+ html! {
337
+ <>
338
+ <label id={props.title.as_deref().unwrap_or("gradient-label").to_owned()} />
339
+ <div class="color-gradient-container gradient-stops-selector">
340
+ <div
341
+ class={if props.discrete {"gradient-stops-bar discrete"} else {"gradient-stops-bar"}}
342
+ ref={bar_ref}
343
+ style={css_gradient(&display)}
344
+ onclick={on_dblclick}
345
+ >
346
+ { for display.iter().enumerate().map(|(index, stop)| html! {
347
+ <div
348
+ class="gradient-stop-handle"
349
+ key={index.to_string()}
350
+ style={format!("left:{}", track_position(stop.offset))}
351
+ >
352
+ <span
353
+ class={classes!("gradient-stop-grip", (!can_drag).then_some("disabled"))}
354
+ data-index={index.to_string()}
355
+ onpointerdown={can_drag.then(|| on_pointerdown.clone())}
356
+ onpointermove={can_drag.then(|| on_pointermove.clone())}
357
+ onpointerup={can_drag.then(|| on_pointerup.clone())}
358
+ ondblclick={can_drag.then(|| on_grip_dblclick.clone())}
359
+ />
360
+ if pinned {
361
+ <span class="gradient-stop-lock" />
362
+ } else {
363
+ <span
364
+ class={classes!("gradient-stop-remove", (!can_remove).then_some("disabled"))}
365
+ data-index={index.to_string()}
366
+ onclick={can_remove.then(|| on_remove.clone())}
367
+ />
368
+ }
369
+ <input
370
+ class="parameter"
371
+ type="color"
372
+ value={stop.color.to_owned()}
373
+ data-index={index.to_string()}
374
+ oninput={on_color.clone()}
375
+ />
376
+ </div>
377
+ }) }
378
+ </div>
379
+ if props.is_modified {
380
+ <span class="reset-default-style" onclick={on_reset} />
381
+ } else {
382
+ <span class="reset-default-style-disabled" />
383
+ }
384
+ </div>
385
+ </>
386
+ }
387
+ }
388
+
389
+ #[cfg(test)]
390
+ mod tests {
391
+ use super::*;
392
+
393
+ fn stop(color: &str, offset: f64) -> GradientStopSpec {
394
+ GradientStopSpec {
395
+ color: color.to_owned(),
396
+ offset,
397
+ }
398
+ }
399
+
400
+ #[test]
401
+ fn sample_clamps_outside_stop_range() {
402
+ let stops = vec![stop("#000000", 0.25), stop("#ff0000", 0.75)];
403
+ assert_eq!(sample_gradient_hex(&stops, 0.0), "#000000");
404
+ assert_eq!(sample_gradient_hex(&stops, 1.0), "#ff0000");
405
+ }
406
+
407
+ #[test]
408
+ fn sample_interpolates_linearly() {
409
+ let stops = vec![stop("#000000", 0.0), stop("#ff0000", 1.0)];
410
+ assert_eq!(sample_gradient_hex(&stops, 0.5), "#800000");
411
+
412
+ let stops = vec![
413
+ stop("#000000", 0.0),
414
+ stop("#ffffff", 0.5),
415
+ stop("#ff0000", 1.0),
416
+ ];
417
+
418
+ assert_eq!(sample_gradient_hex(&stops, 0.25), "#808080");
419
+ assert_eq!(sample_gradient_hex(&stops, 0.75), "#ff8080");
420
+ }
421
+
422
+ #[test]
423
+ fn track_positions_pad_the_bar_edges() {
424
+ assert_eq!(track_position(0.0), "calc(8px + (100% - 16px) * 0.0000)");
425
+ assert_eq!(track_position(1.0), "calc(8px + (100% - 16px) * 1.0000)");
426
+ assert_eq!(track_position(0.333), "calc(8px + (100% - 16px) * 0.3330)");
427
+ }
428
+
429
+ #[test]
430
+ fn gradient_interpolates_on_padded_track() {
431
+ let stops = vec![
432
+ stop("#333333", 1.0),
433
+ stop("#111111", 0.0),
434
+ stop("#222222", 0.5),
435
+ ];
436
+
437
+ assert_eq!(
438
+ css_gradient(&stops),
439
+ format!(
440
+ "background:linear-gradient(to right, #111111 {}, #222222 {}, #333333 {})",
441
+ track_position(0.0),
442
+ track_position(0.5),
443
+ track_position(1.0)
444
+ )
445
+ );
446
+ }
447
+
448
+ #[test]
449
+ fn equalize_centers_between_neighbors_or_edges() {
450
+ let stops = vec![
451
+ stop("#111111", 0.2),
452
+ stop("#222222", 0.25),
453
+ stop("#333333", 0.4),
454
+ ];
455
+
456
+ assert_eq!(equalized_offset(&stops, 1), (0.2 + 0.4) / 2.0);
457
+
458
+ assert_eq!(equalized_offset(&stops, 0), 0.125);
459
+ assert_eq!(equalized_offset(&stops, 2), (0.25 + 1.0) / 2.0);
460
+
461
+ let single = vec![stop("#111111", 0.9)];
462
+ assert_eq!(equalized_offset(&single, 0), 0.5);
463
+ }
464
+
465
+ #[test]
466
+ fn sample_degenerate_cases() {
467
+ assert_eq!(sample_gradient_hex(&[], 0.5), "#000000");
468
+ let stops = vec![stop("#123456", 0.5)];
469
+ assert_eq!(sample_gradient_hex(&stops, 0.1), "#123456");
470
+ assert_eq!(sample_gradient_hex(&stops, 0.9), "#123456");
471
+
472
+ let stops = vec![stop("red", 0.0), stop("blue", 1.0)];
473
+ assert_eq!(sample_gradient_hex(&stops, 0.25), "red");
474
+ assert_eq!(sample_gradient_hex(&stops, 0.75), "blue");
475
+ }
476
+ }
@@ -0,0 +1,86 @@
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 std::rc::Rc;
14
+
15
+ use wasm_bindgen::JsCast;
16
+ use web_sys::{Event, HtmlSelectElement, MouseEvent};
17
+ use yew::prelude::*;
18
+
19
+ use crate::config::{CssKind, NamedValue};
20
+
21
+ /// The named-value controls above a CSS-valued style field: a stateless
22
+ /// quick-action `<select>` over every named value of the field's kind,
23
+ /// and a Pin button that force-adds the field's value to the workspace
24
+ /// palette.
25
+ #[derive(Properties, PartialEq)]
26
+ pub struct NamedValuePickerProps {
27
+ pub kind: CssKind,
28
+ pub entries: Rc<Vec<NamedValue>>,
29
+
30
+ /// Loads the named entry's value into the field.
31
+ pub on_select: Callback<String>,
32
+
33
+ #[prop_or_default]
34
+ pub can_pin: bool,
35
+
36
+ #[prop_or_default]
37
+ pub on_pin: Callback<()>,
38
+ }
39
+
40
+ const PROMPT: &str = "";
41
+
42
+ #[function_component(NamedValuePicker)]
43
+ pub fn named_value_picker(props: &NamedValuePickerProps) -> Html {
44
+ let kind = props.kind;
45
+ let onchange = use_callback(props.on_select.clone(), |event: Event, on_select| {
46
+ let Some(select) = event
47
+ .target()
48
+ .and_then(|x| x.dyn_into::<HtmlSelectElement>().ok())
49
+ else {
50
+ return;
51
+ };
52
+
53
+ let name = select.value();
54
+ select.set_value(PROMPT);
55
+ if !name.is_empty() {
56
+ on_select.emit(name);
57
+ }
58
+ });
59
+
60
+ let on_pin = use_callback(props.on_pin.clone(), |event: MouseEvent, on_pin| {
61
+ event.prevent_default();
62
+ on_pin.emit(());
63
+ });
64
+
65
+ html! {
66
+ <div class="named-value-controls">
67
+ if !props.entries.is_empty() {
68
+ <select class="named-value-select" title="Load a named value" {onchange}>
69
+ <option value={PROMPT} selected=true disabled=true>{ "Load" }</option>
70
+ { for props.entries.iter().map(|entry| html! {
71
+ <option key={entry.name.clone()} value={entry.name.clone()}>
72
+ { kind.short_name(&entry.name) }
73
+ </option>
74
+ }) }
75
+ </select>
76
+ }
77
+ if props.can_pin {
78
+ <span
79
+ class="named-value-pin"
80
+ title="Pin this value to the workspace palette"
81
+ onmousedown={on_pin}
82
+ />
83
+ }
84
+ </div>
85
+ }
86
+ }