@perspective-dev/viewer 5.3.1 → 5.4.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 (255) hide show
  1. package/dist/cdn/perspective-viewer.js +6 -6
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/blueprint.css +1 -1
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -1
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -0
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -1
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -1
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -1
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/extensions.d.ts +0 -5
  31. package/dist/esm/perspective-viewer.inline.js +9 -9
  32. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  33. package/dist/esm/perspective-viewer.js +5 -5
  34. package/dist/esm/perspective-viewer.js.map +2 -2
  35. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +1 -1
  36. package/dist/wasm/perspective-viewer.d.ts +62 -73
  37. package/dist/wasm/perspective-viewer.js +226 -238
  38. package/dist/wasm/perspective-viewer.wasm +0 -0
  39. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -28
  40. package/package.json +7 -7
  41. package/src/css/_context_menu.css +1 -1
  42. package/src/css/_dropdown_menu.css +1 -1
  43. package/src/css/_viewer_bundle.css +7 -8
  44. package/src/css/column-dropdown.css +0 -3
  45. package/src/css/column-selector.css +8 -5
  46. package/src/css/column-settings-panel.css +0 -225
  47. package/src/css/column-style.css +13 -0
  48. package/src/css/config-selector.css +18 -10
  49. package/src/css/filter-dropdown.css +5 -5
  50. package/src/css/form/debug.css +28 -4
  51. package/src/css/function-dropdown.css +5 -5
  52. package/src/css/panel-tab.css +23 -0
  53. package/src/css/plugin-selector.css +5 -5
  54. package/src/css/render-warning.css +1 -1
  55. package/src/css/status-bar.css +52 -21
  56. package/src/css/type-icon.css +1 -3
  57. package/src/css/{containers → ui/containers}/context-menu.css +22 -16
  58. package/src/css/ui/containers/dropdown-menu-items.css +66 -0
  59. package/src/css/{containers → ui/containers}/dropdown-menu.css +8 -53
  60. package/src/css/viewer.css +32 -26
  61. package/src/rust/components/chat_panel.rs +1 -1
  62. package/src/rust/components/{form/code_editor.rs → code_editor.rs} +4 -2
  63. package/src/rust/components/column_dropdown.rs +2 -2
  64. package/src/rust/components/column_selector/active_column.rs +15 -9
  65. package/src/rust/components/column_selector/aggregate_selector.rs +1 -1
  66. package/src/rust/components/column_selector/config_selector.rs +2 -2
  67. package/src/rust/components/column_selector/filter_column.rs +8 -5
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -2
  69. package/src/rust/components/column_selector/pivot_column.rs +4 -3
  70. package/src/rust/components/column_selector/sort_column.rs +7 -4
  71. package/src/rust/components/column_selector.rs +3 -3
  72. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +1 -1
  73. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +15 -17
  74. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +1 -1
  75. package/src/rust/components/column_settings_sidebar/style_tab.rs +4 -5
  76. package/src/rust/components/column_settings_sidebar.rs +3 -4
  77. package/src/rust/components/copy_dropdown.rs +1 -1
  78. package/src/rust/components/datetime_column_style/custom.rs +1 -3
  79. package/src/rust/components/datetime_column_style/simple.rs +1 -2
  80. package/src/rust/components/datetime_column_style.rs +1 -2
  81. package/src/rust/components/{form/debug.rs → debug_panel.rs} +5 -5
  82. package/src/rust/components/{containers/dragdrop_list.rs → dragdrop_list.rs} +2 -10
  83. package/src/rust/components/export_dropdown.rs +1 -1
  84. package/src/rust/components/expression_editor.rs +1 -1
  85. package/src/rust/components/filter_dropdown.rs +2 -2
  86. package/src/rust/components/function_dropdown.rs +2 -2
  87. package/src/rust/components/main_panel/frame_theme.rs +3 -59
  88. package/src/rust/components/main_panel/reconcile.rs +9 -4
  89. package/src/rust/components/main_panel/render.rs +62 -11
  90. package/src/rust/components/main_panel/update.rs +39 -10
  91. package/src/rust/components/main_panel.rs +26 -2
  92. package/src/rust/components/mod.rs +5 -7
  93. package/src/rust/components/new_panel_menu.rs +97 -0
  94. package/src/rust/components/number_series_style.rs +2 -3
  95. package/src/rust/components/panel_menu.rs +61 -87
  96. package/src/rust/components/panel_tab.rs +100 -43
  97. package/src/rust/components/plugin_tab.rs +1 -1
  98. package/src/rust/components/render_warning.rs +2 -2
  99. package/src/rust/components/{status_bar_counter.rs → rows_counter.rs} +10 -10
  100. package/src/rust/components/settings_panel.rs +2 -2
  101. package/src/rust/components/status_bar.rs +113 -19
  102. package/src/rust/components/status_indicator.rs +5 -14
  103. package/src/rust/components/string_column_style.rs +1 -2
  104. package/src/rust/components/{style/mod.rs → style.rs} +32 -15
  105. package/src/rust/components/style_controls/number_string_format/digits_section.rs +3 -5
  106. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  107. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  108. package/src/rust/components/viewer/msg.rs +43 -30
  109. package/src/rust/components/viewer/panels.rs +50 -19
  110. package/src/rust/components/viewer/render.rs +15 -13
  111. package/src/rust/components/viewer/settings.rs +137 -68
  112. package/src/rust/components/viewer/snapshots.rs +5 -10
  113. package/src/rust/components/viewer/wiring.rs +1 -21
  114. package/src/rust/components/viewer.rs +15 -10
  115. package/src/rust/components/window_editor.rs +8 -9
  116. package/src/rust/config/column_config_schema.rs +2 -27
  117. package/src/rust/config/mod.rs +0 -2
  118. package/src/rust/config/workspace_config.rs +2 -1
  119. package/src/rust/custom_elements/debug_plugin.rs +1 -10
  120. package/src/rust/custom_elements/mod.rs +0 -3
  121. package/src/rust/custom_elements/viewer.rs +0 -18
  122. package/src/rust/js/plugin.rs +0 -86
  123. package/src/rust/lib.rs +12 -15
  124. package/src/rust/presentation/column_locator.rs +3 -7
  125. package/src/rust/presentation.rs +68 -38
  126. package/src/rust/queries/hosted_tables.rs +38 -0
  127. package/src/rust/queries/mod.rs +2 -0
  128. package/src/rust/queries/palette_set.rs +1 -1
  129. package/src/rust/renderer/activate.rs +1 -3
  130. package/src/rust/renderer/dispatch.rs +83 -22
  131. package/src/rust/renderer/limits.rs +24 -5
  132. package/src/rust/renderer/plugin_config.rs +1 -0
  133. package/src/rust/renderer/props.rs +2 -1
  134. package/src/rust/renderer.rs +14 -27
  135. package/src/rust/session/props.rs +4 -3
  136. package/src/rust/session/view_subscription.rs +19 -6
  137. package/src/rust/session.rs +38 -70
  138. package/src/rust/tasks/create_panel.rs +1 -1
  139. package/src/rust/tasks/dismiss_render_warning.rs +1 -1
  140. package/src/rust/tasks/eject.rs +1 -0
  141. package/src/rust/tasks/pipeline.rs +0 -60
  142. package/src/rust/tasks/presize_panels.rs +24 -1
  143. package/src/rust/tasks/restore_and_render.rs +0 -17
  144. package/src/rust/tasks/send_column_config.rs +1 -1
  145. package/src/rust/tasks/send_plugin_config.rs +1 -1
  146. package/src/rust/tasks/set_edit_mode.rs +1 -1
  147. package/src/rust/{components → ui}/containers/control_group.rs +25 -3
  148. package/src/rust/{components → ui}/containers/dropdown_menu.rs +5 -2
  149. package/src/rust/{components → ui}/containers/mod.rs +0 -4
  150. package/src/rust/{components → ui}/containers/scroll_panel.rs +0 -22
  151. package/src/rust/{components → ui}/containers/scroll_panel_item.rs +0 -13
  152. package/src/rust/{components → ui}/containers/select.rs +0 -6
  153. package/src/rust/{components → ui}/containers/sidebar.rs +5 -32
  154. package/src/rust/{components → ui}/containers/split_panel.rs +8 -80
  155. package/src/rust/{components → ui}/containers/tab_list.rs +15 -9
  156. package/src/rust/{components → ui}/context_menu.rs +8 -13
  157. package/src/rust/{presentation/drag_helpers.rs → ui/dragdrop.rs} +26 -31
  158. package/src/rust/{components → ui}/font_loader.rs +0 -26
  159. package/src/rust/{components → ui}/form/color_selector.rs +4 -2
  160. package/src/rust/ui/form/intl_label.rs +81 -0
  161. package/src/rust/{components → ui}/form/mirrored_textarea.rs +0 -53
  162. package/src/rust/{components → ui}/form/mod.rs +1 -10
  163. package/src/rust/{components → ui}/form/multi_stop_gradient_selector.rs +4 -5
  164. package/src/rust/{components → ui}/form/named_value_picker.rs +1 -6
  165. package/src/rust/{components → ui}/form/number_field.rs +1 -1
  166. package/src/rust/{components → ui}/form/number_range_field.rs +1 -1
  167. package/src/rust/{components → ui}/form/optional_field.rs +3 -1
  168. package/src/rust/{components → ui}/form/palette_selector.rs +4 -5
  169. package/src/rust/{components → ui}/form/select_enum_field.rs +2 -2
  170. package/src/rust/{components → ui}/form/select_value_field.rs +2 -2
  171. package/src/rust/ui/mod.rs +56 -0
  172. package/src/rust/ui/modal.rs +61 -0
  173. package/src/rust/{components → ui}/portal.rs +174 -79
  174. package/src/rust/{components/style/style_provider.rs → ui/style.rs} +4 -34
  175. package/src/rust/utils/completion.rs +0 -6
  176. package/src/rust/{config/css_scale.rs → utils/css.rs} +38 -35
  177. package/src/rust/utils/mod.rs +2 -0
  178. package/src/rust/workspace.rs +35 -0
  179. package/src/svg/boolean-type.svg +3 -2
  180. package/src/svg/checkbox-checked-icon.svg +4 -7
  181. package/src/svg/checkbox-hover.svg +3 -3
  182. package/src/svg/checkbox-off.svg +2 -3
  183. package/src/svg/checkbox-on.svg +3 -3
  184. package/src/svg/checkbox-unchecked-icon.svg +3 -3
  185. package/src/svg/datagrid-select-column.svg +8 -15
  186. package/src/svg/datagrid-select-region.svg +8 -15
  187. package/src/svg/datagrid-select-row-tree.svg +7 -12
  188. package/src/svg/datagrid-select-row.svg +8 -13
  189. package/src/svg/date-type.svg +4 -23
  190. package/src/svg/drawer-tab-hover.svg +4 -6
  191. package/src/svg/drawer-tab-invert-hover.svg +5 -5
  192. package/src/svg/drawer-tab-invert.svg +3 -5
  193. package/src/svg/drawer-tab.svg +5 -5
  194. package/src/svg/mega-menu-icons-candlestick.svg +5 -37
  195. package/src/svg/mega-menu-icons-ohlc.svg +4 -39
  196. package/src/svg/mega-menu-icons-sunburst.svg +4 -38
  197. package/src/svg/mega-menu-icons-treemap.svg +3 -43
  198. package/src/svg/mega-menu-icons-y-scatter.svg +12 -72
  199. package/src/svg/new-panel-icon.svg +4 -0
  200. package/src/svg/number-type.svg +3 -3
  201. package/src/svg/radio-hover.svg +3 -3
  202. package/src/svg/radio-off.svg +3 -3
  203. package/src/svg/radio-on.svg +3 -3
  204. package/src/svg/status_ok.svg +3 -26
  205. package/src/svg/string-type.svg +4 -8
  206. package/src/svg/theme-icon.svg +5 -5
  207. package/src/themes/blueprint.css +0 -57
  208. package/src/themes/botanical.css +0 -36
  209. package/src/themes/defaults.css +1 -24
  210. package/src/themes/dracula.css +0 -58
  211. package/src/themes/eggplant.css +0 -58
  212. package/src/themes/gruvbox-dark.css +0 -65
  213. package/src/themes/gruvbox.css +1 -39
  214. package/src/themes/icons.css +1 -8
  215. package/src/themes/intl/ar.css +185 -0
  216. package/src/themes/intl/de.css +45 -35
  217. package/src/themes/intl/es.css +40 -30
  218. package/src/themes/intl/fr.css +39 -29
  219. package/src/themes/intl/ja.css +41 -31
  220. package/src/themes/intl/pt.css +37 -27
  221. package/src/themes/intl/zh.css +47 -37
  222. package/src/themes/intl.css +16 -3
  223. package/src/themes/ledger.css +1 -59
  224. package/src/themes/monokai.css +0 -51
  225. package/src/themes/nord.css +0 -55
  226. package/src/themes/phosphor.css +0 -48
  227. package/src/themes/pro-dark.css +0 -37
  228. package/src/themes/pro.css +3 -23
  229. package/src/themes/solarized-dark.css +0 -31
  230. package/src/themes/solarized.css +1 -24
  231. package/src/themes/vaporwave.css +0 -44
  232. package/src/themes/velvet.css +0 -60
  233. package/src/ts/extensions.ts +0 -13
  234. package/src/ts/ts-rs/ViewOnUpdateResp.ts +1 -1
  235. package/src/css/plugin-settings-panel.css +0 -171
  236. package/src/rust/components/modal.rs +0 -177
  237. package/src/rust/custom_elements/copy_dropdown.rs +0 -219
  238. package/src/rust/custom_elements/export_dropdown.rs +0 -216
  239. package/src/rust/custom_elements/modal.rs +0 -299
  240. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline0.js +0 -0
  241. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline1.js +0 -0
  242. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline2.js +0 -0
  243. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline3.js +0 -0
  244. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline4.js +0 -0
  245. /package/dist/wasm/snippets/{perspective-viewer-87bd21b215d0631a → perspective-viewer-adc6f3f79141112b}/inline5.js +0 -0
  246. /package/src/css/{containers → ui/containers}/scroll-panel.css +0 -0
  247. /package/src/css/{containers → ui/containers}/split-panel.css +0 -0
  248. /package/src/css/{containers → ui/containers}/tabs.css +0 -0
  249. /package/src/css/{dom → ui/dom}/checkbox.css +0 -0
  250. /package/src/css/{dom → ui/dom}/scrollbar.css +0 -0
  251. /package/src/css/{dom → ui/dom}/select.css +0 -0
  252. /package/src/css/{form → ui/form}/mirrored-textarea.css +0 -0
  253. /package/src/rust/components/{form → code_editor}/highlight.rs +0 -0
  254. /package/src/rust/{components → ui}/containers/sidebar_close_button.rs +0 -0
  255. /package/src/rust/{components → ui}/form/number_input.rs +0 -0
@@ -22,15 +22,15 @@ use yew::prelude::*;
22
22
 
23
23
  use super::PerspectiveViewer;
24
24
  use super::msg::PerspectiveViewerMsg::*;
25
+ use super::msg::{Divider, PaneTarget};
25
26
  use crate::components::column_settings_sidebar::ColumnSettingsPanel;
26
- use crate::components::containers::split_panel::SplitPanel;
27
- use crate::components::font_loader::FontLoader;
28
27
  use crate::components::main_panel::MainPanel;
29
28
  use crate::components::panel_menu::PanelCommand;
30
29
  use crate::components::settings_panel::SettingsPanel;
31
- use crate::components::style::StyleProvider;
30
+ use crate::components::style::StyleSurface;
32
31
  use crate::queries::*;
33
32
  use crate::session::TableLoadState;
33
+ use crate::ui::{FontLoader, SplitPanel, StyleProvider};
34
34
 
35
35
  impl PerspectiveViewer {
36
36
  pub(super) fn render(&self, ctx: &Context<Self>) -> Html {
@@ -58,10 +58,6 @@ impl PerspectiveViewer {
58
58
  toggle: true,
59
59
  });
60
60
 
61
- let on_column_settings_panel_resize = ctx
62
- .link()
63
- .callback(|(x, _)| ColumnSettingsPanelSizeUpdate(Some(x)));
64
-
65
61
  let on_close_settings = ctx
66
62
  .link()
67
63
  .callback(|()| ToggleSettingsInit(None, true, None));
@@ -135,9 +131,12 @@ impl PerspectiveViewer {
135
131
  id="modal_panel"
136
132
  class={classes!(is_pinned.then_some("pinned"))}
137
133
  reverse=true
134
+ deferred={is_pinned}
135
+ size={self.settings_geometry.column_settings_width_override}
138
136
  initial_size={self.settings_geometry.column_settings_width_override}
139
- on_reset={ctx.link().callback(|_| ColumnSettingsPanelSizeUpdate(None))}
140
- on_resize={on_column_settings_panel_resize}
137
+ on_reset={ctx.link().callback(|_| DividerMove(Divider::ColumnSettings, PaneTarget::Natural))}
138
+ on_resize={ctx.link().callback(|(x, _): (i32, i32)| DividerMove(Divider::ColumnSettings, PaneTarget::Width(x)))}
139
+ on_resize_finished={ctx.link().callback(|_| DividerFinish(Divider::ColumnSettings))}
141
140
  >
142
141
  <ColumnSettingsPanel
143
142
  {selected_column}
@@ -171,6 +170,7 @@ impl PerspectiveViewer {
171
170
  {on_reset}
172
171
  on_activate_panel={ctx.link().callback(|id| SetActivePanel(id, None))}
173
172
  on_close_panel={ctx.link().callback(|id| ClosePanel(id, None))}
173
+ on_panel_closed={ctx.link().callback(PanelClosed)}
174
174
  on_panel_command={ctx.link().batch_callback(|(id, cmd): (String, PanelCommand)| {
175
175
  match cmd {
176
176
  PanelCommand::New => vec![NewPanel(id)],
@@ -178,6 +178,7 @@ impl PerspectiveViewer {
178
178
  vec![NewPanelFrom { client, table }]
179
179
  },
180
180
  PanelCommand::Duplicate => vec![DuplicatePanel(id)],
181
+ PanelCommand::NewFromPanel(source) => vec![DuplicatePanel(source)],
181
182
  PanelCommand::Reset => vec![ResetPanel(Some(id), false, None)],
182
183
  PanelCommand::ToggleMaster => vec![ToggleMaster(id)],
183
184
  PanelCommand::Close => vec![ClosePanel(id, None)],
@@ -233,8 +234,9 @@ impl PerspectiveViewer {
233
234
  };
234
235
 
235
236
  html! {
236
- <StyleProvider root={ctx.props().elem.clone()}>
237
+ <StyleProvider root={ctx.props().elem.clone()} sheet={StyleSurface::Viewer.sheet()}>
237
238
  <div id="component_container" class={is_single_panel}>
239
+ <slot name="modal" />
238
240
  <div id="layout_area">
239
241
  <SplitPanel
240
242
  id="app_panel"
@@ -243,9 +245,9 @@ impl PerspectiveViewer {
243
245
  deferred=true
244
246
  size={self.settings_geometry.pane_width_override}
245
247
  initial_size={self.settings_geometry.pane_width_override}
246
- on_reset={ctx.link().callback(|_| SettingsPanelSizeUpdate(None))}
247
- on_resize={ctx.link().callback(|(x, _): (i32, i32)| SettingsDividerMove(x))}
248
- on_resize_finished={ctx.link().callback(|_| SettingsDividerFinish)}
248
+ on_reset={ctx.link().callback(|_| DividerMove(Divider::Settings, PaneTarget::Natural))}
249
+ on_resize={ctx.link().callback(|(x, _): (i32, i32)| DividerMove(Divider::Settings, PaneTarget::Width(x)))}
250
+ on_resize_finished={ctx.link().callback(|_| DividerFinish(Divider::Settings))}
249
251
  >
250
252
  { settings_panel }
251
253
  <div id="main_column_container">
@@ -10,11 +10,6 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- //! Settings-sidebar handlers: the open/close toggle (with its presize
14
- //! choreography), the deferred divider's latest-wins presize pump
15
- //! (`PRESIZE_EVERYWHERE_PLAN.md` P1/P2), and the column-settings drawer. The
16
- //! presize sweeps themselves live in [`crate::tasks`] (`presize_panels`).
17
-
18
13
  use futures::channel::oneshot::{Sender, channel};
19
14
  use perspective_js::utils::*;
20
15
  use wasm_bindgen::prelude::*;
@@ -22,12 +17,13 @@ use yew::prelude::*;
22
17
 
23
18
  use super::PerspectiveViewer;
24
19
  use super::msg::PerspectiveViewerMsg::*;
25
- use crate::components::font_loader::FontLoaderStatus;
20
+ use super::msg::{Divider, PaneTarget};
26
21
  use crate::components::settings_panel::SelectedTab;
27
22
  use crate::config::*;
28
23
  use crate::presentation::{ColumnSettingsTab, ColumnSettingsTarget, OpenColumnSettings};
29
24
  use crate::queries::get_current_column_locator;
30
25
  use crate::tasks::*;
26
+ use crate::ui::FontLoaderStatus;
31
27
 
32
28
  /// The settings sidebar's geometry state, folded into one field on
33
29
  /// [`PerspectiveViewer`]: the pane/drawer width overrides, the divider
@@ -44,11 +40,9 @@ pub(super) struct SettingsGeometry {
44
40
  /// High-water-mark auto width reported by the settings panel.
45
41
  pub auto_width: f64,
46
42
 
47
- /// Latest-wins presize pump for the (deferred) settings divider: the
48
- /// newest proposed pane width not yet presized, and whether a pump
49
- /// iteration is in flight. See `SettingsDividerMove`.
50
- pub divider_target: Option<i32>,
51
- pub divider_pumping: bool,
43
+ pub settings_divider: DividerPump,
44
+
45
+ pub column_settings_divider: DividerPump,
52
46
 
53
47
  /// Open-state geometry deltas `(layout_area.w − mpc.w, main_column.h −
54
48
  /// mpc.h)`, cached at settings *close* time — used to presize panels to
@@ -85,6 +79,29 @@ pub(super) struct SettingsGeometry {
85
79
  pub column_settings_docked_width: Option<f64>,
86
80
  }
87
81
 
82
+ #[derive(Default)]
83
+ pub(super) struct DividerPump {
84
+ pub target: Option<PaneTarget>,
85
+ pub pumping: bool,
86
+ pub finish_pending: bool,
87
+ }
88
+
89
+ impl SettingsGeometry {
90
+ fn pump_mut(&mut self, divider: Divider) -> &mut DividerPump {
91
+ match divider {
92
+ Divider::Settings => &mut self.settings_divider,
93
+ Divider::ColumnSettings => &mut self.column_settings_divider,
94
+ }
95
+ }
96
+
97
+ fn is_divider_deferred(&self, divider: Divider) -> bool {
98
+ match divider {
99
+ Divider::Settings => true,
100
+ Divider::ColumnSettings => self.column_settings_pinned,
101
+ }
102
+ }
103
+ }
104
+
88
105
  impl PerspectiveViewer {
89
106
  pub(super) fn on_toggle_settings_init(
90
107
  &mut self,
@@ -257,84 +274,143 @@ impl PerspectiveViewer {
257
274
  };
258
275
  }
259
276
 
260
- pub(super) fn on_settings_panel_size_update(&mut self, x: Option<i32>) -> bool {
261
- match x {
262
- Some(x) => {
263
- self.settings_geometry.pane_width_override = Some(x);
264
- false
277
+ fn apply_pane_target(&mut self, divider: Divider, target: PaneTarget) -> bool {
278
+ let deferred = self.settings_geometry.is_divider_deferred(divider);
279
+ match (divider, target) {
280
+ (Divider::Settings, PaneTarget::Width(w)) => {
281
+ self.settings_geometry.pane_width_override = Some(w);
282
+ true
265
283
  },
266
- None => {
284
+ (Divider::Settings, PaneTarget::Natural) => {
267
285
  self.settings_geometry.pane_width_override = None;
268
286
  self.settings_geometry.auto_width = 0.0;
269
287
  self.on_settings_panel_dimensions_reset.emit(());
270
288
  true
271
289
  },
290
+ (Divider::ColumnSettings, PaneTarget::Width(w)) => {
291
+ self.settings_geometry.column_settings_width_override = Some(w);
292
+ deferred
293
+ },
294
+ (Divider::ColumnSettings, PaneTarget::Natural) => {
295
+ self.settings_geometry.column_settings_width_override = None;
296
+ self.settings_geometry.column_settings_auto_width = 0.0;
297
+ true
298
+ },
272
299
  }
273
300
  }
274
301
 
275
- pub(super) fn on_settings_divider_move(
302
+ pub(super) fn on_divider_move(
276
303
  &mut self,
277
304
  ctx: &Context<Self>,
278
- pane_width: i32,
305
+ divider: Divider,
306
+ target: PaneTarget,
279
307
  ) -> bool {
308
+ if !self.settings_geometry.is_divider_deferred(divider) {
309
+ return self.apply_pane_target(divider, target);
310
+ }
311
+
280
312
  // Latest-wins: overwrite the pending target; start the pump if
281
313
  // idle. Intermediate targets that arrive while a presize is in
282
314
  // flight are dropped (mirrors `PresizeQueue`'s single queued
283
315
  // slot) — the pane tracks the pointer at content-render rate.
284
- self.settings_geometry.divider_target = Some(pane_width);
285
- if !self.settings_geometry.divider_pumping {
286
- self.settings_geometry.divider_pumping = true;
287
- ctx.link().send_message(SettingsDividerPump);
316
+ let pump = self.settings_geometry.pump_mut(divider);
317
+ pump.target = Some(target);
318
+ if !pump.pumping {
319
+ pump.pumping = true;
320
+ ctx.link().send_message(DividerPump(divider));
288
321
  }
289
322
 
290
323
  false
291
324
  }
292
325
 
293
- pub(super) fn on_settings_divider_pump(&mut self, ctx: &Context<Self>) -> bool {
294
- if let Some(pane_width) = self.settings_geometry.divider_target.take() {
295
- let workspace = ctx.props().workspace.clone();
296
- let elem = ctx.props().elem.clone();
297
- let link = ctx.link().clone();
298
- ApiFuture::spawn(async move {
299
- let presents =
300
- presize_visible_panels_pane_width(&workspace, &elem, pane_width as f64).await;
301
- link.send_message(SettingsDividerCommit(pane_width));
302
- // Same task as the commit's re-render (whether Yew drained it
303
- // synchronously inside `send_message` or deferred it to a
304
- // microtask): the staged reveal and the pane-width geometry
305
- // land in one paint.
306
- presents.reveal();
307
- Ok(())
308
- });
309
- } else {
310
- self.settings_geometry.divider_pumping = false;
311
- }
326
+ pub(super) fn on_divider_pump(&mut self, ctx: &Context<Self>, divider: Divider) -> bool {
327
+ let workspace = ctx.props().workspace.clone();
328
+ let pump = self.settings_geometry.pump_mut(divider);
329
+ let Some(target) = pump.target.take() else {
330
+ pump.pumping = false;
331
+ if std::mem::take(&mut pump.finish_pending) {
332
+ ApiFuture::spawn(async move {
333
+ resize_visible_panels(&workspace).await;
334
+ Ok(())
335
+ });
336
+ }
337
+
338
+ return false;
339
+ };
340
+
341
+ let elem = ctx.props().elem.clone();
342
+ let pane_width = match target {
343
+ PaneTarget::Width(w) => Some(w as f64),
344
+ PaneTarget::Natural => measure_pane_natural_width(&elem, divider.pane_selector()),
345
+ };
346
+
347
+ let link = ctx.link().clone();
348
+ ApiFuture::spawn(async move {
349
+ let presents = match pane_width {
350
+ Some(pane_width) => {
351
+ presize_visible_panels_pane_width(
352
+ &workspace,
353
+ &elem,
354
+ divider.pane_selector(),
355
+ pane_width,
356
+ )
357
+ .await
358
+ },
359
+ None => StagedPresents::default(),
360
+ };
361
+
362
+ link.send_message(DividerCommit(divider, target));
363
+ presents.reveal();
364
+ Ok(())
365
+ });
312
366
 
313
367
  false
314
368
  }
315
369
 
316
- pub(super) fn on_settings_divider_commit(
370
+ pub(super) fn on_divider_commit(
317
371
  &mut self,
318
372
  ctx: &Context<Self>,
319
- pane_width: i32,
373
+ divider: Divider,
374
+ target: PaneTarget,
320
375
  ) -> bool {
321
376
  // Every visible panel has rendered at (approximately) its
322
377
  // target box — NOW move the geometry: the re-render below
323
- // applies this width to the deferred `SplitPanel`'s controlled
378
+ // applies this target to the deferred `SplitPanel`'s controlled
324
379
  // `size`, in the same task as the presizes' inline clears.
325
- self.settings_geometry.pane_width_override = Some(pane_width);
326
- ctx.link().send_message(SettingsDividerPump);
380
+ self.apply_pane_target(divider, target);
381
+ if target == PaneTarget::Natural {
382
+ self.settings_geometry.pump_mut(divider).finish_pending = true;
383
+ }
384
+
385
+ ctx.link().send_message(DividerPump(divider));
327
386
  true
328
387
  }
329
388
 
330
- pub(super) fn on_settings_divider_finish(&mut self, ctx: &Context<Self>) -> bool {
389
+ pub(super) fn on_divider_finish(&mut self, ctx: &Context<Self>, divider: Divider) -> bool {
390
+ if !self.settings_geometry.is_divider_deferred(divider) {
391
+ return false;
392
+ }
393
+
394
+ let pump = self.settings_geometry.pump_mut(divider);
395
+ if pump.pumping {
396
+ pump.finish_pending = true;
397
+ } else {
398
+ let workspace = ctx.props().workspace.clone();
399
+ ApiFuture::spawn(async move {
400
+ resize_visible_panels(&workspace).await;
401
+ Ok(())
402
+ });
403
+ }
404
+
405
+ false
406
+ }
407
+
408
+ fn resize_for_ratchet(&self, ctx: &Context<Self>) {
331
409
  let workspace = ctx.props().workspace.clone();
332
410
  ApiFuture::spawn(async move {
333
411
  resize_visible_panels(&workspace).await;
334
412
  Ok(())
335
413
  });
336
-
337
- false
338
414
  }
339
415
 
340
416
  pub(super) fn on_settings_panel_tab_changed(&mut self, tab: SelectedTab) -> bool {
@@ -343,9 +419,10 @@ impl PerspectiveViewer {
343
419
  changed
344
420
  }
345
421
 
346
- pub(super) fn on_settings_panel_auto_width(&mut self, w: f64) -> bool {
422
+ pub(super) fn on_settings_panel_auto_width(&mut self, ctx: &Context<Self>, w: f64) -> bool {
347
423
  if w > self.settings_geometry.auto_width {
348
424
  self.settings_geometry.auto_width = w;
425
+ self.resize_for_ratchet(ctx);
349
426
  true
350
427
  } else {
351
428
  false
@@ -397,19 +474,17 @@ impl PerspectiveViewer {
397
474
  true
398
475
  }
399
476
 
400
- pub(super) fn on_column_settings_panel_size_update(&mut self, x: Option<i32>) -> bool {
401
- self.settings_geometry.column_settings_width_override = x;
402
- if x.is_none() {
403
- self.settings_geometry.column_settings_auto_width = 0.0;
404
- true
405
- } else {
406
- false
407
- }
408
- }
409
-
410
- pub(super) fn on_column_settings_panel_auto_width(&mut self, w: f64) -> bool {
477
+ pub(super) fn on_column_settings_panel_auto_width(
478
+ &mut self,
479
+ ctx: &Context<Self>,
480
+ w: f64,
481
+ ) -> bool {
411
482
  if w > self.settings_geometry.column_settings_auto_width {
412
483
  self.settings_geometry.column_settings_auto_width = w;
484
+ if self.settings_geometry.column_settings_pinned {
485
+ self.resize_for_ratchet(ctx);
486
+ }
487
+
413
488
  true
414
489
  } else {
415
490
  false
@@ -561,12 +636,6 @@ impl PerspectiveViewer {
561
636
  true
562
637
  }
563
638
 
564
- /// Toggling the debug tab re-renders the settings panel only — the
565
- /// `DebugPanel` populates itself (`get_viewer_config` on mount +
566
- /// change subscriptions), so no plugin dispatch is owed. The old
567
- /// `just_render` here relied on the pre-amendment unconditional
568
- /// `Unchanged → update` arm and repainted the plugin as a side effect
569
- /// (`PLUGIN_DRAW_INVARIANT_PLAN.md` amendment, migration 2).
570
639
  pub(super) fn on_toggle_debug(&mut self, _ctx: &Context<Self>) -> bool {
571
640
  self.debug_open = !self.debug_open;
572
641
  true
@@ -22,7 +22,7 @@ use yew::prelude::*;
22
22
  use super::PerspectiveViewer;
23
23
  use crate::presentation::{DragDropProps, PresentationProps};
24
24
  use crate::renderer::RendererProps;
25
- use crate::session::{SessionProps, TableLoadState, ViewStats};
25
+ use crate::session::{SessionProps, TableLoadState};
26
26
  use crate::tasks::resize_visible_panels;
27
27
 
28
28
  impl PerspectiveViewer {
@@ -59,12 +59,12 @@ impl PerspectiveViewer {
59
59
 
60
60
  pub(super) fn on_update_session_stats(
61
61
  &mut self,
62
- stats: Option<ViewStats>,
62
+ has_table_cells: bool,
63
63
  has_table: Option<TableLoadState>,
64
64
  ) -> bool {
65
- let changed =
66
- stats != self.session_props.stats || has_table != self.session_props.has_table;
67
- self.session_props.stats = stats;
65
+ let changed = has_table_cells != self.session_props.has_table_cells
66
+ || has_table != self.session_props.has_table;
67
+ self.session_props.has_table_cells = has_table_cells;
68
68
  self.session_props.has_table = has_table;
69
69
  changed
70
70
  }
@@ -146,11 +146,6 @@ impl PerspectiveViewer {
146
146
  changed
147
147
  }
148
148
 
149
- /// LEVEL-triggered spinner count: ASSIGN the absolute in-flight
150
- /// config-run count from the session's RAII accounting — any missed or
151
- /// reordered notification is corrected by the next one, unlike the
152
- /// edge-counted increment/decrement pair this replaces (see
153
- /// `UPDATE_COUNT_REGRESSION_PLAN.md`).
154
149
  pub(super) fn on_update_in_flight(&mut self, count: u32) -> bool {
155
150
  let changed = count != self.update_count;
156
151
  self.update_count = count;
@@ -280,28 +280,14 @@ pub(super) fn create_active_subscriptions(
280
280
 
281
281
  let sub1 = session.table_loaded.add_notify_listener(&cb);
282
282
  let sub2 = session.table_unloaded.add_notify_listener(&cb);
283
- // Snapshot refresh on every RECONCILED commit (SKIP/REUSE/paused
284
- // included), not just `View` construction.
285
283
  let sub3 = session.commit_reconciled.add_notify_listener(&cb);
286
284
  let sub4 = session.view_config_changed.add_notify_listener(&cb);
287
285
  let sub5 = session.title_changed.add_notify_listener(&cb);
288
-
289
- // The `StatusIndicator` spinner count. LEVEL-triggered: the payload
290
- // is the ABSOLUTE in-flight config-run count (RAII-settled by
291
- // `ConfigRunToken`), which the handler ASSIGNS — replacing the
292
- // edge-counted `view_config_changed`+1/`view_created`−1 pair, whose
293
- // pairing the pipeline does not guarantee (stuck-spinner
294
- // regression — see `UPDATE_COUNT_REGRESSION_PLAN.md`).
295
286
  let sub6 = session
296
287
  .run_state_changed
297
288
  .add_listener(ctx.link().callback(UpdateInFlight));
298
289
 
299
- // Stats fetch resolution (populates session.column_stats) triggers
300
- // a fresh `SessionProps` so `column_stats` reaches downstream
301
- // components and the StyleTab re-queries the schema with the
302
- // new value.
303
290
  let sub7 = session.column_stats_changed.add_notify_listener(&cb);
304
-
305
291
  vec![sub1, sub2, sub3, sub4, sub5, sub6, sub7]
306
292
  };
307
293
 
@@ -311,12 +297,6 @@ pub(super) fn create_active_subscriptions(
311
297
  move |_: JsPerspectiveViewerPlugin| UpdateRenderer(Box::new(renderer.to_props(None)))
312
298
  });
313
299
 
314
- // Re-snapshot RendererProps when the plugin_config bucket
315
- // changes (in-tab edit via `send_plugin_config`, JSON paste via
316
- // `restore_and_render`, full clear via `reset_all` with
317
- // `all=true`). Without this, `RendererProps.plugin_config`
318
- // would stay frozen at its construct-time value and `PluginTab`
319
- // would render stale.
320
300
  let cb_plugin_config = ctx.link().callback({
321
301
  let renderer = renderer.clone();
322
302
  move |_: serde_json::Map<String, serde_json::Value>| {
@@ -413,7 +393,7 @@ pub(super) fn inject_active_callbacks(
413
393
  let s = session.clone();
414
394
  let cb = ctx
415
395
  .link()
416
- .callback(move |_: ()| UpdateSessionStats(s.get_table_stats(), s.has_table()));
396
+ .callback(move |_: ()| UpdateSessionStats(s.has_table_cells(), s.has_table()));
417
397
 
418
398
  *session.on_stats_changed.borrow_mut() = Some(cb);
419
399
  }
@@ -27,6 +27,7 @@ mod settings;
27
27
  mod snapshots;
28
28
  mod wiring;
29
29
 
30
+ use std::collections::HashMap;
30
31
  use std::rc::Rc;
31
32
 
32
33
  use futures::channel::oneshot::Sender;
@@ -40,13 +41,13 @@ pub use self::msg::PerspectiveViewerMsg;
40
41
  use self::msg::PerspectiveViewerMsg::*;
41
42
  use self::settings::SettingsGeometry;
42
43
  use self::wiring::*;
43
- use super::font_loader::{FontLoaderProps, FontLoaderStatus};
44
44
  use crate::presentation::{Presentation, PresentationProps};
45
45
  use crate::renderer::{RendererProps, *};
46
46
  use crate::session::{SessionProps, *};
47
47
  use crate::tasks::*;
48
+ use crate::ui::{FontLoaderProps, FontLoaderStatus};
48
49
  use crate::utils::*;
49
- use crate::workspace::Workspace;
50
+ use crate::workspace::{PanelId, Workspace};
50
51
 
51
52
  #[derive(Clone, Properties)]
52
53
  pub struct PerspectiveViewerProps {
@@ -99,6 +100,10 @@ pub struct PerspectiveViewer {
99
100
  on_settings_panel_dimensions_reset: Rc<PubSub<()>>,
100
101
  settings_open: bool,
101
102
 
103
+ /// `removePanel()` completions awaiting their panel's `PanelClosed`
104
+ /// (the deferred eject — see `ClosePanel`).
105
+ pending_closes: HashMap<PanelId, Completion>,
106
+
102
107
  /// Render snapshot of the `Workspace`-owned global filter set (see
103
108
  /// `Workspace::global_filters`); refreshed by `UpdateGlobalFilters` via
104
109
  /// the `filters_changed` subscription. The status-bar chips render from
@@ -210,6 +215,7 @@ impl Component for PerspectiveViewer {
210
215
  on_resize: Default::default(),
211
216
  on_settings_panel_dimensions_reset: Default::default(),
212
217
  settings_open: false,
218
+ pending_closes: HashMap::new(),
213
219
  global_filters: Vec::new(),
214
220
  settings_geometry: Default::default(),
215
221
  session_props,
@@ -305,6 +311,7 @@ impl Component for PerspectiveViewer {
305
311
  LayoutChanged => self.on_layout_changed(ctx),
306
312
  SetActivePanel(id, completion) => self.on_set_active_panel(ctx, id, completion),
307
313
  ClosePanel(id, completion) => self.on_close_panel(ctx, id, completion),
314
+ PanelClosed(id) => self.on_panel_closed(ctx, id),
308
315
  CommitWorkspaceRestore(id) => self.on_commit_workspace_restore(ctx, id),
309
316
  DuplicatePanel(id) => self.on_duplicate_panel(ctx, id),
310
317
  NewPanel(id) => self.on_new_panel(ctx, id),
@@ -325,20 +332,18 @@ impl Component for PerspectiveViewer {
325
332
  ToggleSettingsComplete(update, resolve) => {
326
333
  self.on_toggle_settings_complete(ctx, update, resolve)
327
334
  },
328
- SettingsPanelSizeUpdate(x) => self.on_settings_panel_size_update(x),
329
- SettingsDividerMove(w) => self.on_settings_divider_move(ctx, w),
330
- SettingsDividerPump => self.on_settings_divider_pump(ctx),
331
- SettingsDividerCommit(w) => self.on_settings_divider_commit(ctx, w),
332
- SettingsDividerFinish => self.on_settings_divider_finish(ctx),
335
+ DividerMove(divider, target) => self.on_divider_move(ctx, divider, target),
336
+ DividerPump(divider) => self.on_divider_pump(ctx, divider),
337
+ DividerCommit(divider, target) => self.on_divider_commit(ctx, divider, target),
338
+ DividerFinish(divider) => self.on_divider_finish(ctx, divider),
333
339
  SettingsPanelTabChanged(tab) => self.on_settings_panel_tab_changed(tab),
334
- SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(w),
340
+ SettingsPanelAutoWidth(w) => self.on_settings_panel_auto_width(ctx, w),
335
341
  OpenColumnSettings {
336
342
  target,
337
343
  sender,
338
344
  toggle,
339
345
  } => self.on_open_column_settings(ctx, target, sender, toggle),
340
- ColumnSettingsPanelSizeUpdate(x) => self.on_column_settings_panel_size_update(x),
341
- ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(w),
346
+ ColumnSettingsPanelAutoWidth(w) => self.on_column_settings_panel_auto_width(ctx, w),
342
347
  ToggleColumnSettingsPin => self.on_toggle_column_settings_pin(ctx),
343
348
  ToggleColumnSettingsPinComplete(resolve) => {
344
349
  self.on_toggle_column_settings_pin_complete(resolve)
@@ -20,12 +20,10 @@ use yew::prelude::*;
20
20
 
21
21
  use crate::components::column_dropdown::{ColumnDropDownElement, ColumnDropDownPortal};
22
22
  use crate::components::column_selector::{ColumnSelectorColumnRow, InPlaceColumn, PivotColumn};
23
- use crate::components::containers::dragdrop_list::{
24
- DragContext, DragDropList, DragDropListItemProps,
25
- };
26
- use crate::components::containers::select::{Select, SelectItem};
23
+ use crate::components::dragdrop_list::{DragContext, DragDropList, DragDropListItemProps};
27
24
  use crate::presentation::Presentation;
28
25
  use crate::session::{Session, SessionMetadataRc};
26
+ use crate::ui::{IntlLabel, Select, SelectItem};
29
27
  use crate::utils::{AddListener, DragEffect, DragTarget, Subscription};
30
28
 
31
29
  /// The declared capabilities of one window aggregate, for a `source` column
@@ -479,8 +477,9 @@ impl Component for WindowSlotColumn {
479
477
  let presentation = ctx.props().presentation.clone();
480
478
  let action = ctx.props().action;
481
479
  move |event: DragEvent| {
482
- presentation.set_drag_image(&event).unwrap();
483
- presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
480
+ if presentation.set_drag_image(&event) {
481
+ presentation.notify_drag_start(column.clone(), DragEffect::Move(action))
482
+ }
484
483
  }
485
484
  });
486
485
 
@@ -909,7 +908,7 @@ impl Component for WindowEditor {
909
908
  <div id="window-editor-slots">{ source_list }{ order_list }{ partition_list }</div>
910
909
  <div id="window-editor-container">
911
910
  if show_frame {
912
- <div class="column-style-label"><label id="window-frame-label" /></div>
911
+ <div class="column-style-label"><IntlLabel name="window-frame" /></div>
913
912
  <div class="row">
914
913
  <Select<String>
915
914
  id="window-frame-type"
@@ -943,7 +942,7 @@ impl Component for WindowEditor {
943
942
  }
944
943
  if show_offset {
945
944
  <div class="column-style-label">
946
- <label id="window-offset-label" class="indent" />
945
+ <IntlLabel name="window-offset" class="indent" />
947
946
  </div>
948
947
  <div class="row">
949
948
  <input
@@ -957,7 +956,7 @@ impl Component for WindowEditor {
957
956
  }
958
957
  if show_alpha {
959
958
  <div class="column-style-label">
960
- <label id="window-alpha-label" class="indent" />
959
+ <IntlLabel name="window-alpha" class="indent" />
961
960
  </div>
962
961
  <div class="row">
963
962
  <input
@@ -16,9 +16,9 @@ use serde::{Deserialize, Serialize};
16
16
  use serde_json::Value;
17
17
 
18
18
  use super::{
19
- CssKind, CustomNumberFormatConfig, DatetimeFormatType, KeyValueOpts,
20
- NumberSeriesStyleDefaultConfig,
19
+ CustomNumberFormatConfig, DatetimeFormatType, KeyValueOpts, NumberSeriesStyleDefaultConfig,
21
20
  };
21
+ use crate::utils::{CssKind, GradientStopSpec, canonicalize_gradient_stops};
22
22
 
23
23
  /// The full schema for one column at one point in time. Plugins may return
24
24
  /// different schemas for the same column based on the column's current
@@ -154,31 +154,6 @@ pub struct EnumVariant {
154
154
  pub label: Option<String>,
155
155
  }
156
156
 
157
- /// One stop of a [`ControlSpec::GradientStops`] value in its in-memory
158
- /// form: a `#rrggbb` color at `offset` ∈ `[0, 1]`.
159
- #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
160
- pub struct GradientStopSpec {
161
- pub color: String,
162
- pub offset: f64,
163
- }
164
-
165
- /// The [`ControlSpec::GradientStops`] canonical stop order: offsets
166
- /// clamped to `[0, 1]` and rounded to 3 decimals, stops sorted stably
167
- /// by offset.
168
- pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
169
- for stop in &mut stops {
170
- stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
171
- }
172
-
173
- stops.sort_by(|a, b| {
174
- a.offset
175
- .partial_cmp(&b.offset)
176
- .unwrap_or(std::cmp::Ordering::Equal)
177
- });
178
-
179
- stops
180
- }
181
-
182
157
  /// Fit `stops` to a `discrete` field's fixed pair: an over-length value
183
158
  /// keeps only its two end colors, pinned to `0`/`1`.
184
159
  pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {