1use crate::config::{KeyBindings, NumberFormat, ResolvedColumnFormat, TextAlignment};
11use crate::data::{compare_cells, CellValue, Column, ColumnKind};
12use crate::format::format_cell;
13use crate::grid::selection::{ScrollbarAxis, SortDirection};
14use crate::grid::state::{FilterValueRow, SCROLLBAR_SIZE};
15use crate::grid::theme::GridTheme;
16use crate::pivot::aggregation::AggregationFn;
17use crate::pivot::config::{PivotConfig, PivotZone};
18use crate::pivot::context_menu::{
19 PivotCellContext, PivotContextMenuProviderHandle, PivotContextMenuRequest, PivotMenuItem,
20 PivotMenuTarget, PivotPathComponent, PIVOT_ACTION_COPY_CSV, PIVOT_ACTION_COPY_VALUE,
21 PIVOT_ACTION_SHOW_SOURCE_ROWS,
22};
23use crate::pivot::engine::{compute_pivot, PivotNode, PivotResult, TOTAL_KEY};
24
25use gpui::{px, App, Bounds, FocusHandle, Pixels, Point, ScrollHandle, Size};
26use std::collections::{HashMap, HashSet};
27use std::rc::Rc;
28use std::sync::Arc;
29
30pub type PivotSaveConfigHandler = Rc<dyn Fn(&PivotConfig, &mut App)>;
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum VisibleRowKind {
37 Leaf,
39 GroupHeader {
42 expanded: bool,
44 },
45 GrandTotal,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct VisibleRow {
52 pub key: usize,
55 pub depth: usize,
57 pub kind: VisibleRowKind,
59 pub zebra: bool,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum VisibleColKind {
67 Leaf,
69 Collapsed,
71 Subtotal,
74 GrandTotal,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct VisibleCol {
81 pub key: usize,
84 pub depth: usize,
86 pub kind: VisibleColKind,
88}
89
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum PivotSortKey {
93 RowLabel,
95 RowsByColumn(usize),
99 ColLabel,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum PivotHitResult {
107 None,
109 Corner,
111 ColHeader {
113 level: usize,
115 col: usize,
117 },
118 RowHeader {
120 row: usize,
122 },
123 RowBorder {
125 row: usize,
127 },
128 ColBorder {
130 col: usize,
132 },
133 RowHeaderBorder,
135 RowChevron {
137 row: usize,
139 },
140 ColChevron {
142 col: usize,
144 depth: usize,
146 },
147 Cell {
149 row: usize,
151 col: usize,
153 },
154 VScrollbar,
156 HScrollbar,
158}
159
160#[derive(Clone, Debug)]
162pub struct PivotFilterPopover {
163 pub field: usize,
165 pub rows: Vec<FilterValueRow>,
167 pub anchor: Point<Pixels>,
169}
170
171impl PivotFilterPopover {
172 #[must_use]
174 pub fn all_checked(&self) -> bool {
175 !self.rows.is_empty() && self.rows.iter().all(|r| r.checked)
176 }
177}
178
179#[derive(Clone, Copy, Debug)]
182pub struct PivotFormatDialog {
183 pub field: usize,
185 pub zone: PivotZone,
189 pub anchor: Point<Pixels>,
191}
192
193#[derive(Clone, Debug)]
196pub(crate) struct PivotMenu {
197 pub(crate) anchor: Point<Pixels>,
199 pub(crate) items: Vec<PivotMenuItem>,
201 pub(crate) hovered: Option<usize>,
203 pub(crate) request: PivotContextMenuRequest,
205 pub(crate) drill: Vec<(usize, HashSet<String>)>,
208}
209
210pub(crate) const ROW_INDENT: f32 = 16.0;
212pub(crate) const CHEVRON_SIZE: f32 = 14.0;
214const RESIZE_HIT_SLOP: f32 = 3.0;
215pub const DEFAULT_PIVOT_ROW_HEIGHT: f32 = 24.0;
217pub const DEFAULT_PIVOT_COLUMN_WIDTH: f32 = 140.0;
219pub const MIN_PIVOT_ROW_HEIGHT: f32 = 18.0;
221pub const MIN_PIVOT_COLUMN_WIDTH: f32 = 40.0;
223pub const MIN_PIVOT_ROW_HEADER_WIDTH: f32 = 60.0;
225pub const DEFAULT_PIVOT_SIDEBAR_WIDTH: f32 = 260.0;
227
228#[derive(Clone, Copy, Debug)]
229enum PivotResizeDrag {
230 Row {
231 boundary: usize,
232 start_y: f32,
233 start_height: f32,
234 },
235 Column {
236 boundary: usize,
237 start_x: f32,
238 start_width: f32,
239 },
240 RowHeader {
241 start_x: f32,
242 start_width: f32,
243 },
244}
245
246pub struct PivotState {
248 pub config: PivotConfig,
252 pub result: Arc<PivotResult>,
254 pub(crate) source_rows: Arc<Vec<Vec<CellValue>>>,
256 pub(crate) source_columns: Vec<Column>,
258 pub(crate) resolved_formats: Vec<ResolvedColumnFormat>,
260 pub(crate) label_formats: Vec<ResolvedColumnFormat>,
264 pub(crate) value_fmt: ResolvedColumnFormat,
266
267 pub(crate) visible_rows: Arc<Vec<VisibleRow>>,
270 pub(crate) visible_cols: Arc<Vec<VisibleCol>>,
272 pub(crate) collapsed_row_paths: HashSet<Vec<String>>,
275 pub(crate) collapsed_col_paths: HashSet<Vec<String>>,
277 pub sort: Option<(PivotSortKey, SortDirection)>,
279 pub(crate) filter_values: HashMap<usize, HashSet<String>>,
282 pub(crate) filter_popover: Option<PivotFilterPopover>,
284 pub(crate) format_dialog: Option<PivotFormatDialog>,
286 pub agg_menu_open: bool,
288 pub(crate) save_config_handler: Option<PivotSaveConfigHandler>,
291 pub(crate) sidebar_width: f32,
294 pub(crate) context_menu_provider: Option<PivotContextMenuProviderHandle>,
296 pub(crate) menu: Option<PivotMenu>,
298 pub(crate) pending_drill_down: Option<Vec<(usize, HashSet<String>)>>,
303
304 pub selection: Option<(usize, usize, usize, usize)>,
307 pub(crate) select_anchor: Option<(usize, usize)>,
308 pub(crate) is_selecting: bool,
309 pub hover_hit: Option<PivotHitResult>,
311 pub(crate) scrollbar_drag: Option<ScrollbarAxis>,
312 resize_drag: Option<PivotResizeDrag>,
313
314 pub theme: GridTheme,
316 pub animations: bool,
320 pub(crate) key_bindings: KeyBindings,
321 pub scroll_handle: ScrollHandle,
323 pub focus_handle: FocusHandle,
325 pub bounds: Bounds<Pixels>,
327 pub(crate) window_viewport: Size<Pixels>,
328 pub row_height: f32,
330 pub header_row_height: f32,
332 pub row_header_width: f32,
334 pub value_col_width: f32,
336 pub font_size: f32,
338 pub char_width: f32,
340}
341
342impl PivotState {
343 #[must_use]
346 pub fn new(
347 source_columns: Vec<Column>,
348 source_rows: Arc<Vec<Vec<CellValue>>>,
349 resolved_formats: Vec<ResolvedColumnFormat>,
350 config: PivotConfig,
351 key_bindings: KeyBindings,
352 focus_handle: FocusHandle,
353 ) -> Self {
354 let mut state = Self {
355 config,
356 result: Arc::new(PivotResult::default()),
357 source_rows,
358 source_columns,
359 resolved_formats,
360 label_formats: Vec::new(),
361 value_fmt: default_value_format(),
362 visible_rows: Arc::new(Vec::new()),
363 visible_cols: Arc::new(Vec::new()),
364 collapsed_row_paths: HashSet::new(),
365 collapsed_col_paths: HashSet::new(),
366 sort: None,
367 filter_values: HashMap::new(),
368 filter_popover: None,
369 format_dialog: None,
370 agg_menu_open: false,
371 save_config_handler: None,
372 sidebar_width: DEFAULT_PIVOT_SIDEBAR_WIDTH,
373 context_menu_provider: None,
374 menu: None,
375 pending_drill_down: None,
376 selection: None,
377 select_anchor: None,
378 is_selecting: false,
379 hover_hit: None,
380 scrollbar_drag: None,
381 theme: GridTheme::default(),
382 animations: true,
383 key_bindings,
384 scroll_handle: ScrollHandle::new(),
385 focus_handle,
386 bounds: Bounds::default(),
387 window_viewport: Size::default(),
388 row_height: DEFAULT_PIVOT_ROW_HEIGHT,
389 header_row_height: 26.0,
390 row_header_width: 220.0,
391 value_col_width: DEFAULT_PIVOT_COLUMN_WIDTH,
392 font_size: 14.0,
395 char_width: crate::grid::paint::default_char_width(14.0),
396 resize_drag: None,
397 };
398 state.recompute();
399 state
400 }
401
402 pub fn set_source(&mut self, columns: Vec<Column>, rows: Arc<Vec<Vec<CellValue>>>) {
405 self.source_columns = columns;
406 self.source_rows = rows;
407 self.recompute();
408 }
409
410 #[must_use]
412 pub fn source_differs(&self, rows: &Arc<Vec<Vec<CellValue>>>) -> bool {
413 !Arc::ptr_eq(&self.source_rows, rows)
414 }
415
416 #[must_use]
418 pub fn source_columns(&self) -> &[Column] {
419 &self.source_columns
420 }
421
422 pub fn recompute(&mut self) {
425 self.config.clamp_to_columns(self.source_columns.len());
426 let filter_fields = self.config.filter_fields.clone();
427 self.filter_values
428 .retain(|field, _| filter_fields.contains(field));
429 self.label_formats = self.build_label_formats();
430
431 if self.config.is_ready() {
432 let included = self.filtered_source_rows();
433 self.result = Arc::new(compute_pivot(
434 &self.source_columns,
435 &self.source_rows,
436 &included,
437 &self.config,
438 &self.label_formats,
439 ));
440 } else {
441 self.result = Arc::new(PivotResult::default());
442 }
443 self.value_fmt = self.build_value_format();
444 self.resort();
445 }
446
447 fn build_label_formats(&self) -> Vec<ResolvedColumnFormat> {
451 self.resolved_formats
452 .iter()
453 .enumerate()
454 .map(|(i, base)| {
455 let mut fmt = base.clone();
456 if let Some(over) = self.config.field_formats.get(&i) {
457 fmt.number = *over;
458 fmt.string.alignment = over.alignment;
459 fmt.date.alignment = over.alignment;
460 fmt.boolean.alignment = over.alignment;
461 }
462 fmt
463 })
464 .collect()
465 }
466
467 #[must_use]
471 pub fn label_format(&self, field: usize) -> Option<&ResolvedColumnFormat> {
472 self.label_formats.get(field)
473 }
474
475 fn filtered_source_rows(&self) -> Vec<usize> {
477 let active: Vec<(usize, &HashSet<String>)> = self
478 .config
479 .filter_fields
480 .iter()
481 .filter_map(|&f| self.filter_values.get(&f).map(|set| (f, set)))
482 .collect();
483 if active.is_empty() {
484 return (0..self.source_rows.len()).collect();
485 }
486 (0..self.source_rows.len())
487 .filter(|&r| {
488 active.iter().all(|(field, allowed)| {
489 let cell = self.source_rows[r].get(*field).unwrap_or(&CellValue::None);
490 let label = format_cell(cell, &self.label_formats[*field]).0;
491 allowed.contains(&label)
492 })
493 })
494 .collect()
495 }
496
497 fn build_value_format(&self) -> ResolvedColumnFormat {
505 let mut fmt = self
506 .config
507 .value_field
508 .and_then(|f| self.resolved_formats.get(f).cloned())
509 .unwrap_or_else(default_value_format);
510 match self.config.aggregation {
511 AggregationFn::Count => {
512 fmt.kind = ColumnKind::Integer;
513 fmt.number = NumberFormat {
514 decimals: 0,
515 ..fmt.number
516 };
517 }
518 AggregationFn::Avg => fmt.kind = ColumnKind::Decimal,
519 AggregationFn::Sum | AggregationFn::Min | AggregationFn::Max => {}
520 }
521 fmt.number.alignment = TextAlignment::Right;
522 fmt.string.alignment = TextAlignment::Right;
523 fmt.date.alignment = TextAlignment::Right;
524 fmt.boolean.alignment = TextAlignment::Right;
525 fmt.number.show_negative_red = true;
526 if let Some(over) = self.config.value_format {
527 fmt.number = over;
528 fmt.string.alignment = over.alignment;
529 fmt.date.alignment = over.alignment;
530 fmt.boolean.alignment = over.alignment;
531 }
532 fmt
533 }
534
535 #[must_use]
537 pub fn value_format(&self) -> &ResolvedColumnFormat {
538 &self.value_fmt
539 }
540
541 pub(crate) fn rebuild_visible(&mut self) {
548 self.visible_rows = Arc::new(flatten_rows(
549 &self.result,
550 &self.collapsed_row_paths,
551 &self.config,
552 ));
553 self.visible_cols = Arc::new(flatten_cols(
554 &self.result,
555 &self.collapsed_col_paths,
556 &self.config,
557 ));
558 self.clamp_selection();
559 self.clamp_scroll_to_bounds();
560 }
561
562 #[must_use]
564 pub fn visible_rows(&self) -> &[VisibleRow] {
565 &self.visible_rows
566 }
567
568 #[must_use]
570 pub fn visible_cols(&self) -> &[VisibleCol] {
571 &self.visible_cols
572 }
573
574 pub fn toggle_row_group(&mut self, node: usize) {
577 if node >= self.result.row_nodes.len() {
578 return;
579 }
580 let path = node_path(&self.result.row_nodes, node);
581 if !self.collapsed_row_paths.remove(&path) {
582 self.collapsed_row_paths.insert(path);
583 }
584 self.rebuild_visible();
585 }
586
587 pub fn toggle_col_group(&mut self, node: usize) {
589 if node >= self.result.col_nodes.len() {
590 return;
591 }
592 let path = node_path(&self.result.col_nodes, node);
593 if !self.collapsed_col_paths.remove(&path) {
594 self.collapsed_col_paths.insert(path);
595 }
596 self.rebuild_visible();
597 }
598
599 pub fn collapse_all_rows(&mut self) {
601 self.collapsed_row_paths = all_group_paths(&self.result.row_nodes);
602 self.rebuild_visible();
603 }
604
605 pub fn expand_all_rows(&mut self) {
607 self.collapsed_row_paths.clear();
608 self.rebuild_visible();
609 }
610
611 pub fn collapse_all_cols(&mut self) {
613 self.collapsed_col_paths = all_group_paths(&self.result.col_nodes);
614 self.rebuild_visible();
615 }
616
617 pub fn expand_all_cols(&mut self) {
619 self.collapsed_col_paths.clear();
620 self.rebuild_visible();
621 }
622
623 pub fn cycle_sort_by_column(&mut self, col_key: usize) {
630 self.sort = match self.sort {
631 Some((PivotSortKey::RowsByColumn(k), SortDirection::Ascending)) if k == col_key => {
632 Some((
633 PivotSortKey::RowsByColumn(col_key),
634 SortDirection::Descending,
635 ))
636 }
637 Some((PivotSortKey::RowsByColumn(k), SortDirection::Descending)) if k == col_key => {
638 None
639 }
640 _ => Some((
641 PivotSortKey::RowsByColumn(col_key),
642 SortDirection::Ascending,
643 )),
644 };
645 self.resort();
646 }
647
648 pub fn cycle_label_sort(&mut self) {
651 self.sort = match self.sort {
652 Some((PivotSortKey::RowLabel, SortDirection::Ascending)) => {
653 Some((PivotSortKey::RowLabel, SortDirection::Descending))
654 }
655 Some((PivotSortKey::RowLabel, SortDirection::Descending)) => None,
656 _ => Some((PivotSortKey::RowLabel, SortDirection::Ascending)),
657 };
658 self.resort();
659 }
660
661 pub fn cycle_col_label_sort(&mut self) {
663 self.sort = match self.sort {
664 Some((PivotSortKey::ColLabel, SortDirection::Ascending)) => {
665 Some((PivotSortKey::ColLabel, SortDirection::Descending))
666 }
667 Some((PivotSortKey::ColLabel, SortDirection::Descending)) => None,
668 _ => Some((PivotSortKey::ColLabel, SortDirection::Ascending)),
669 };
670 self.resort();
671 }
672
673 pub(crate) fn resort(&mut self) {
677 let result = Arc::make_mut(&mut self.result);
678 sort_axis_by_key(&mut result.row_nodes, &mut result.row_roots, false);
681 sort_axis_by_key(&mut result.col_nodes, &mut result.col_roots, false);
682 match self.sort {
683 None => {}
684 Some((PivotSortKey::RowLabel, dir)) => {
685 sort_axis_by_key(
686 &mut result.row_nodes,
687 &mut result.row_roots,
688 dir == SortDirection::Descending,
689 );
690 }
691 Some((PivotSortKey::ColLabel, dir)) => {
692 sort_axis_by_key(
693 &mut result.col_nodes,
694 &mut result.col_roots,
695 dir == SortDirection::Descending,
696 );
697 }
698 Some((PivotSortKey::RowsByColumn(col_key), dir)) => {
699 let keys: Vec<CellValue> = (0..result.row_nodes.len())
700 .map(|id| {
701 result
702 .values
703 .get(&(id, col_key))
704 .cloned()
705 .unwrap_or(CellValue::None)
706 })
707 .collect();
708 sort_axis_by(
709 &mut result.row_nodes,
710 &mut result.row_roots,
711 &keys,
712 dir == SortDirection::Descending,
713 );
714 }
715 }
716 self.rebuild_visible();
717 }
718
719 pub fn open_filter_popover(&mut self, field: usize, anchor: Point<Pixels>) {
726 if field >= self.source_columns.len() {
727 return;
728 }
729 let fmt = &self.label_formats[field];
730 let allowed = self.filter_values.get(&field);
731 let mut seen: HashSet<String> = HashSet::new();
732 let mut pairs: Vec<(String, CellValue)> = Vec::new();
733 for row in self.source_rows.iter() {
734 let cell = row.get(field).unwrap_or(&CellValue::None);
735 let label = format_cell(cell, fmt).0;
736 if seen.insert(label.clone()) {
737 pairs.push((label, cell.clone()));
738 }
739 }
740 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
741 let rows = pairs
742 .into_iter()
743 .map(|(label, _)| {
744 let checked = allowed.is_none_or(|set| set.contains(&label));
745 FilterValueRow { label, checked }
746 })
747 .collect();
748 self.filter_popover = Some(PivotFilterPopover {
749 field,
750 rows,
751 anchor,
752 });
753 }
754
755 #[must_use]
757 pub fn filter_popover(&self) -> Option<&PivotFilterPopover> {
758 self.filter_popover.as_ref()
759 }
760
761 pub fn toggle_filter_popover_value(&mut self, index: usize) {
763 if let Some(p) = &mut self.filter_popover {
764 if let Some(row) = p.rows.get_mut(index) {
765 row.checked = !row.checked;
766 }
767 }
768 self.apply_filter_popover();
769 }
770
771 pub fn toggle_filter_popover_select_all(&mut self) {
773 if let Some(p) = &mut self.filter_popover {
774 let target = !p.all_checked();
775 for row in &mut p.rows {
776 row.checked = target;
777 }
778 }
779 self.apply_filter_popover();
780 }
781
782 pub fn apply_filter_popover(&mut self) {
785 let Some(p) = &self.filter_popover else {
786 return;
787 };
788 let field = p.field;
789 if p.all_checked() {
790 self.filter_values.remove(&field);
791 } else {
792 self.filter_values.insert(
793 field,
794 p.rows
795 .iter()
796 .filter(|r| r.checked)
797 .map(|r| r.label.clone())
798 .collect(),
799 );
800 }
801 self.recompute();
802 }
803
804 pub fn close_filter_popover(&mut self) {
806 self.filter_popover = None;
807 }
808
809 pub fn clear_filter(&mut self, field: usize) {
811 if self.filter_values.remove(&field).is_some() {
812 self.recompute();
813 }
814 if let Some(p) = &mut self.filter_popover {
815 if p.field == field {
816 for row in &mut p.rows {
817 row.checked = true;
818 }
819 }
820 }
821 }
822
823 #[must_use]
826 pub fn filter_active(&self, field: usize) -> bool {
827 self.filter_values.contains_key(&field)
828 }
829
830 #[must_use]
836 pub fn format_dialog(&self) -> Option<&PivotFormatDialog> {
837 self.format_dialog.as_ref()
838 }
839
840 pub fn open_format_dialog(&mut self, field: usize, zone: PivotZone, anchor: Point<Pixels>) {
843 if field >= self.source_columns.len() {
844 return;
845 }
846 self.format_dialog = Some(PivotFormatDialog {
847 field,
848 zone,
849 anchor,
850 });
851 }
852
853 pub fn close_format_dialog(&mut self) {
855 self.format_dialog = None;
856 }
857
858 #[must_use]
862 pub fn format_dialog_format(&self) -> Option<NumberFormat> {
863 let dialog = self.format_dialog.as_ref()?;
864 let fmt = match dialog.zone {
865 PivotZone::Values => &self.value_fmt,
866 _ => self.label_formats.get(dialog.field)?,
867 };
868 let mut number = fmt.number;
871 number.alignment = fmt.alignment();
872 Some(number)
873 }
874
875 pub fn update_format_dialog(&mut self, mutate: impl FnOnce(&mut NumberFormat)) {
878 let Some(dialog) = self.format_dialog else {
879 return;
880 };
881 let Some(mut fmt) = self.format_dialog_format() else {
882 return;
883 };
884 mutate(&mut fmt);
885 match dialog.zone {
886 PivotZone::Values => self.config.value_format = Some(fmt),
887 _ => {
888 self.config.field_formats.insert(dialog.field, fmt);
889 }
890 }
891 self.recompute();
892 }
893
894 pub fn reset_format_dialog(&mut self) {
897 let Some(dialog) = self.format_dialog else {
898 return;
899 };
900 match dialog.zone {
901 PivotZone::Values => self.config.value_format = None,
902 _ => {
903 self.config.field_formats.remove(&dialog.field);
904 }
905 }
906 self.recompute();
907 }
908
909 pub fn set_context_menu_provider(
917 &mut self,
918 provider: impl crate::pivot::context_menu::PivotContextMenuProvider + 'static,
919 ) {
920 self.context_menu_provider = Some(PivotContextMenuProviderHandle::new(provider));
921 }
922
923 pub fn on_save_config(&mut self, handler: impl Fn(&PivotConfig, &mut App) + 'static) {
927 self.save_config_handler = Some(Rc::new(handler));
928 }
929
930 pub fn clear_save_config_handler(&mut self) {
933 self.save_config_handler = None;
934 }
935
936 #[must_use]
938 pub fn has_save_config_handler(&self) -> bool {
939 self.save_config_handler.is_some()
940 }
941
942 #[must_use]
944 pub fn save_config_handler(&self) -> Option<PivotSaveConfigHandler> {
945 self.save_config_handler.clone()
946 }
947
948 #[must_use]
950 pub fn row_path_components(&self, row_key: usize) -> Vec<PivotPathComponent> {
951 path_components(
952 &self.result.row_nodes,
953 &self.config.row_fields,
954 &self.source_columns,
955 &self.config.blank_label,
956 row_key,
957 )
958 }
959
960 #[must_use]
962 pub fn col_path_components(&self, col_key: usize) -> Vec<PivotPathComponent> {
963 path_components(
964 &self.result.col_nodes,
965 &self.config.column_fields,
966 &self.source_columns,
967 &self.config.blank_label,
968 col_key,
969 )
970 }
971
972 #[must_use]
977 pub fn source_rows_for(&self, row_key: usize, col_key: usize) -> Vec<usize> {
978 let constraints: Vec<(usize, String)> = self
979 .row_path_components(row_key)
980 .into_iter()
981 .chain(self.col_path_components(col_key))
982 .map(|c| (c.field_index, c.label))
983 .collect();
984 rows_matching_path(
985 &self.source_rows,
986 &self.label_formats,
987 &self.config.blank_label,
988 &self.filtered_source_rows(),
989 &constraints,
990 )
991 }
992
993 #[must_use]
999 pub fn drill_down_filters(
1000 &self,
1001 row_key: usize,
1002 col_key: usize,
1003 ) -> Vec<(usize, HashSet<String>)> {
1004 let mut out: Vec<(usize, HashSet<String>)> = self
1005 .config
1006 .filter_fields
1007 .iter()
1008 .filter_map(|&f| self.filter_values.get(&f).map(|set| (f, set.clone())))
1009 .collect();
1010 for comp in self
1011 .row_path_components(row_key)
1012 .into_iter()
1013 .chain(self.col_path_components(col_key))
1014 {
1015 let set = drill_filter_set(&comp.label, &self.config.blank_label);
1016 if let Some(entry) = out.iter_mut().find(|(f, _)| *f == comp.field_index) {
1017 entry.1 = set;
1018 } else {
1019 out.push((comp.field_index, set));
1020 }
1021 }
1022 out
1023 }
1024
1025 pub fn request_drill_down(&mut self, row: usize, col: usize) {
1030 let (Some(vr), Some(vc)) = (
1031 self.visible_rows.get(row).copied(),
1032 self.visible_cols.get(col).copied(),
1033 ) else {
1034 return;
1035 };
1036 self.pending_drill_down = Some(self.drill_down_filters(vr.key, vc.key));
1037 }
1038
1039 pub(crate) fn take_pending_drill_down(&mut self) -> Option<Vec<(usize, HashSet<String>)>> {
1041 self.pending_drill_down.take()
1042 }
1043
1044 fn cell_context(&self, vr: &VisibleRow, vc: &VisibleCol) -> PivotCellContext {
1046 let value = self.result.value(vr.key, vc.key).clone();
1047 let formatted_value = if matches!(value, CellValue::None) {
1048 String::new()
1049 } else {
1050 format_cell(&value, &self.value_fmt).0
1051 };
1052 PivotCellContext {
1053 row_path: self.row_path_components(vr.key),
1054 col_path: self.col_path_components(vc.key),
1055 value,
1056 formatted_value,
1057 is_row_grand_total: vr.kind == VisibleRowKind::GrandTotal,
1058 is_col_grand_total: vc.kind == VisibleColKind::GrandTotal,
1059 is_row_subtotal: matches!(vr.kind, VisibleRowKind::GroupHeader { .. }),
1060 is_col_subtotal: matches!(
1061 vc.kind,
1062 VisibleColKind::Subtotal | VisibleColKind::Collapsed
1063 ),
1064 }
1065 }
1066
1067 pub fn open_context_menu(&mut self, hit: PivotHitResult, anchor: Point<Pixels>) {
1072 self.filter_popover = None;
1073 let resolved: Option<(PivotMenuTarget, usize, usize)> = match hit {
1074 PivotHitResult::Cell { row, col } => {
1075 match (self.visible_rows.get(row), self.visible_cols.get(col)) {
1076 (Some(vr), Some(vc)) => {
1077 let ctx = self.cell_context(vr, vc);
1078 Some((PivotMenuTarget::Cell(ctx), vr.key, vc.key))
1079 }
1080 _ => None,
1081 }
1082 }
1083 PivotHitResult::RowHeader { row } | PivotHitResult::RowChevron { row } => {
1084 self.visible_rows.get(row).map(|vr| {
1085 (
1086 PivotMenuTarget::RowHeader {
1087 path: self.row_path_components(vr.key),
1088 is_grand_total: vr.kind == VisibleRowKind::GrandTotal,
1089 },
1090 vr.key,
1091 TOTAL_KEY,
1092 )
1093 })
1094 }
1095 PivotHitResult::ColHeader { level: 0, .. } | PivotHitResult::Corner => {
1096 Some((PivotMenuTarget::Corner, TOTAL_KEY, TOTAL_KEY))
1097 }
1098 PivotHitResult::ColHeader { level, col } => {
1099 let depth = level - 1;
1100 let key = self
1101 .col_ancestor_at(col, depth)
1102 .or_else(|| self.visible_cols.get(col).map(|vc| vc.key));
1103 key.map(|key| {
1104 let is_grand = key == TOTAL_KEY
1105 && self
1106 .visible_cols
1107 .get(col)
1108 .is_some_and(|vc| vc.kind == VisibleColKind::GrandTotal);
1109 (
1110 PivotMenuTarget::ColHeader {
1111 path: self.col_path_components(key),
1112 is_grand_total: is_grand,
1113 },
1114 TOTAL_KEY,
1115 key,
1116 )
1117 })
1118 }
1119 PivotHitResult::ColChevron { col, depth } => {
1120 self.col_group_run_at(col, depth).map(|(node, _)| {
1121 (
1122 PivotMenuTarget::ColHeader {
1123 path: self.col_path_components(node),
1124 is_grand_total: false,
1125 },
1126 TOTAL_KEY,
1127 node,
1128 )
1129 })
1130 }
1131 PivotHitResult::None
1132 | PivotHitResult::RowBorder { .. }
1133 | PivotHitResult::ColBorder { .. }
1134 | PivotHitResult::RowHeaderBorder
1135 | PivotHitResult::VScrollbar
1136 | PivotHitResult::HScrollbar => None,
1137 };
1138 let Some((target, row_key, col_key)) = resolved else {
1139 self.menu = None;
1140 return;
1141 };
1142
1143 let request = PivotContextMenuRequest {
1144 source_row_indices: self.source_rows_for(row_key, col_key),
1145 target,
1146 aggregation: self.config.aggregation,
1147 value_field_index: self.config.value_field,
1148 value_caption: self.result.value_caption.clone(),
1149 config: self.config.clone(),
1150 };
1151 let items = match &self.context_menu_provider {
1152 Some(provider) => provider.menu_items(&request),
1153 None => PivotMenuItem::standard_items(&request.target),
1154 };
1155 if items.is_empty() {
1156 self.menu = None;
1157 return;
1158 }
1159 let drill = self.drill_down_filters(row_key, col_key);
1160 self.menu = Some(PivotMenu {
1161 anchor,
1162 items,
1163 hovered: None,
1164 request,
1165 drill,
1166 });
1167 }
1168
1169 pub(crate) fn execute_menu_action(&mut self, id: &str, menu: PivotMenu, cx: &mut App) {
1172 match id {
1173 PIVOT_ACTION_SHOW_SOURCE_ROWS => {
1174 self.pending_drill_down = Some(menu.drill);
1175 }
1176 PIVOT_ACTION_COPY_VALUE => {
1177 if let Some(cell) = menu.request.clicked_cell() {
1178 cx.write_to_clipboard(gpui::ClipboardItem::new_string(
1179 cell.formatted_value.clone(),
1180 ));
1181 }
1182 }
1183 PIVOT_ACTION_COPY_CSV => {
1184 let csv = self.to_csv();
1185 if !csv.is_empty() {
1186 cx.write_to_clipboard(gpui::ClipboardItem::new_string(csv));
1187 }
1188 }
1189 _ => {
1190 if let Some(provider) = self.context_menu_provider.clone() {
1191 provider.on_action(id, &menu.request, self, cx);
1192 }
1193 }
1194 }
1195 }
1196
1197 #[must_use]
1203 pub fn row_label(&self, row: &VisibleRow) -> String {
1204 match row.kind {
1205 VisibleRowKind::GrandTotal => "Grand Total".into(),
1206 _ if row.key == TOTAL_KEY => "Total".into(),
1207 _ => self
1208 .result
1209 .row_nodes
1210 .get(row.key)
1211 .map(|n| n.label.clone())
1212 .unwrap_or_default(),
1213 }
1214 }
1215
1216 #[must_use]
1219 pub fn col_label(&self, col: &VisibleCol) -> String {
1220 match col.kind {
1221 VisibleColKind::GrandTotal => "Grand Total".into(),
1222 VisibleColKind::Subtotal | VisibleColKind::Collapsed => self
1223 .result
1224 .col_nodes
1225 .get(col.key)
1226 .map(|n| format!("{} Total", n.label))
1227 .unwrap_or_else(|| "Total".into()),
1228 _ if col.key == TOTAL_KEY => self.result.value_caption.clone(),
1229 _ => self
1230 .result
1231 .col_nodes
1232 .get(col.key)
1233 .map(|n| n.label.clone())
1234 .unwrap_or_default(),
1235 }
1236 }
1237
1238 #[must_use]
1240 pub fn cell_value(&self, row: &VisibleRow, col: &VisibleCol) -> &CellValue {
1241 self.result.value(row.key, col.key)
1242 }
1243
1244 #[must_use]
1250 pub fn row_height(&self) -> f32 {
1251 self.row_height
1252 }
1253
1254 pub fn set_row_height(&mut self, height: f32) {
1259 if height.is_finite() {
1260 self.row_height = height.max(MIN_PIVOT_ROW_HEIGHT);
1261 self.clamp_scroll_to_bounds();
1262 }
1263 }
1264
1265 #[must_use]
1267 pub fn column_width(&self) -> f32 {
1268 self.value_col_width
1269 }
1270
1271 pub fn set_column_width(&mut self, width: f32) {
1276 if width.is_finite() {
1277 self.value_col_width = width.max(MIN_PIVOT_COLUMN_WIDTH);
1278 self.clamp_scroll_to_bounds();
1279 }
1280 }
1281
1282 #[must_use]
1286 pub fn row_header_area_width(&self) -> f32 {
1287 self.row_header_width
1288 }
1289
1290 pub fn set_row_header_width(&mut self, width: f32) {
1295 if width.is_finite() {
1296 self.row_header_width = width.max(MIN_PIVOT_ROW_HEADER_WIDTH);
1297 self.clamp_scroll_to_bounds();
1298 }
1299 }
1300
1301 #[must_use]
1304 pub fn header_levels(&self) -> usize {
1305 1 + self.result.col_depth.max(1)
1306 }
1307
1308 #[must_use]
1310 pub fn header_height(&self) -> f32 {
1311 self.header_levels() as f32 * self.header_row_height
1312 }
1313
1314 #[must_use]
1316 pub fn content_size(&self) -> (f32, f32) {
1317 (
1318 self.visible_cols.len() as f32 * self.value_col_width,
1319 self.visible_rows.len() as f32 * self.row_height,
1320 )
1321 }
1322
1323 pub(crate) fn scrollbar_reserved(&self) -> (f32, f32) {
1324 let (cw, ch) = self.content_size();
1325 let vw = f32::from(self.bounds.size.width) - self.row_header_width;
1326 let vh = f32::from(self.bounds.size.height) - self.header_height();
1327 let reserved_w = if ch > vh { SCROLLBAR_SIZE } else { 0.0 };
1328 let reserved_h = if cw > vw { SCROLLBAR_SIZE } else { 0.0 };
1329 (reserved_w, reserved_h)
1330 }
1331
1332 pub(crate) fn max_scroll(&self) -> (f32, f32) {
1333 let (cw, ch) = self.content_size();
1334 let (rw, rh) = self.scrollbar_reserved();
1335 let vw = f32::from(self.bounds.size.width) - self.row_header_width - rw;
1336 let vh = f32::from(self.bounds.size.height) - self.header_height() - rh;
1337 ((cw - vw).max(0.0), (ch - vh).max(0.0))
1338 }
1339
1340 pub(crate) fn clamp_scroll_to_bounds(&mut self) {
1342 let (mx, my) = self.max_scroll();
1343 let s = self.scroll_handle.offset();
1344 let nx = f32::from(s.x).clamp(0.0, mx);
1345 let ny = f32::from(s.y).clamp(0.0, my);
1346 if nx != f32::from(s.x) || ny != f32::from(s.y) {
1347 self.scroll_handle.set_offset(Point {
1348 x: px(nx),
1349 y: px(ny),
1350 });
1351 }
1352 }
1353
1354 fn clamp_selection(&mut self) {
1355 let (nr, nc) = (self.visible_rows.len(), self.visible_cols.len());
1356 if let Some((r1, c1, r2, c2)) = self.selection {
1357 if nr == 0 || nc == 0 || r1 >= nr || c1 >= nc {
1358 self.selection = None;
1359 self.select_anchor = None;
1360 } else {
1361 self.selection = Some((r1, c1, r2.min(nr - 1), c2.min(nc - 1)));
1362 }
1363 }
1364 }
1365
1366 #[must_use]
1368 pub fn hit_test(&self, pos: Point<Pixels>) -> PivotHitResult {
1369 let x = f32::from(pos.x);
1370 let y = f32::from(pos.y);
1371 let bw = f32::from(self.bounds.size.width);
1372 let bh = f32::from(self.bounds.size.height);
1373 if x < 0.0 || y < 0.0 || x > bw || y > bh {
1374 return PivotHitResult::None;
1375 }
1376 let hdr_h = self.header_height();
1377 let (mx, my) = self.max_scroll();
1378 if my > 0.0 && x >= bw - SCROLLBAR_SIZE && y >= hdr_h {
1379 return PivotHitResult::VScrollbar;
1380 }
1381 if mx > 0.0 && y >= bh - SCROLLBAR_SIZE && x >= self.row_header_width {
1382 return PivotHitResult::HScrollbar;
1383 }
1384 let sx = f32::from(self.scroll_handle.offset().x);
1385 let sy = f32::from(self.scroll_handle.offset().y);
1386
1387 if (x - self.row_header_width).abs() <= RESIZE_HIT_SLOP {
1391 return PivotHitResult::RowHeaderBorder;
1392 }
1393
1394 if y < hdr_h {
1395 if x < self.row_header_width {
1396 return PivotHitResult::Corner;
1397 }
1398 let level = ((y / self.header_row_height) as usize).min(self.header_levels() - 1);
1399 let cx = x - self.row_header_width + sx;
1400 if cx < 0.0 {
1401 return PivotHitResult::None;
1402 }
1403 let boundary = (cx / self.value_col_width).round() as usize;
1404 if boundary > 0
1405 && boundary <= self.visible_cols.len()
1406 && (cx - boundary as f32 * self.value_col_width).abs() <= RESIZE_HIT_SLOP
1407 {
1408 return PivotHitResult::ColBorder { col: boundary - 1 };
1409 }
1410 let col = (cx / self.value_col_width) as usize;
1411 if col >= self.visible_cols.len() {
1412 return PivotHitResult::None;
1413 }
1414 if level >= 1 {
1417 let depth = level - 1;
1418 if let Some((_, run_start)) = self.col_group_run_at(col, depth) {
1419 let run_x0 = run_start as f32 * self.value_col_width;
1420 let within = cx - run_x0;
1421 let is_group = self
1422 .col_ancestor_at(col, depth)
1423 .map(|n| !self.result.col_nodes[n].is_leaf())
1424 .unwrap_or(false);
1425 if is_group && (2.0..=2.0 + CHEVRON_SIZE).contains(&within) {
1426 return PivotHitResult::ColChevron {
1427 col: run_start,
1428 depth,
1429 };
1430 }
1431 }
1432 }
1433 return PivotHitResult::ColHeader { level, col };
1434 }
1435
1436 if x < self.row_header_width {
1437 let ry = y - hdr_h + sy;
1438 if ry < 0.0 {
1439 return PivotHitResult::None;
1440 }
1441 let boundary = (ry / self.row_height).round() as usize;
1442 if boundary > 0
1443 && boundary <= self.visible_rows.len()
1444 && (ry - boundary as f32 * self.row_height).abs() <= RESIZE_HIT_SLOP
1445 {
1446 return PivotHitResult::RowBorder { row: boundary - 1 };
1447 }
1448 let row = (ry / self.row_height) as usize;
1449 if row >= self.visible_rows.len() {
1450 return PivotHitResult::None;
1451 }
1452 let vr = self.visible_rows[row];
1453 if matches!(vr.kind, VisibleRowKind::GroupHeader { .. }) {
1454 let indent = vr.depth as f32 * ROW_INDENT + 4.0;
1455 if x >= indent && x <= indent + CHEVRON_SIZE {
1456 return PivotHitResult::RowChevron { row };
1457 }
1458 }
1459 return PivotHitResult::RowHeader { row };
1460 }
1461
1462 let cx = x - self.row_header_width + sx;
1463 let ry = y - hdr_h + sy;
1464 if cx < 0.0 || ry < 0.0 {
1465 return PivotHitResult::None;
1466 }
1467 let col = (cx / self.value_col_width) as usize;
1468 let row = (ry / self.row_height) as usize;
1469 if row < self.visible_rows.len() && col < self.visible_cols.len() {
1470 PivotHitResult::Cell { row, col }
1471 } else {
1472 PivotHitResult::None
1473 }
1474 }
1475
1476 pub(crate) fn col_group_run_at(&self, col: usize, depth: usize) -> Option<(usize, usize)> {
1480 let node = self.col_ancestor_at(col, depth)?;
1481 let mut start = col;
1482 while start > 0 && self.col_ancestor_at(start - 1, depth) == Some(node) {
1483 start -= 1;
1484 }
1485 Some((node, start))
1486 }
1487
1488 pub(crate) fn col_ancestor_at(&self, col: usize, depth: usize) -> Option<usize> {
1490 let vc = self.visible_cols.get(col)?;
1491 if vc.key == TOTAL_KEY {
1492 return None;
1493 }
1494 let mut id = vc.key;
1495 let mut d = self.result.col_nodes.get(id)?.depth;
1496 if depth > d {
1497 return None;
1498 }
1499 while d > depth {
1500 id = self.result.col_nodes[id].parent?;
1501 d -= 1;
1502 }
1503 Some(id)
1504 }
1505
1506 pub fn handle_mouse_down(&mut self, pos: Point<Pixels>, shift: bool) {
1512 let hit = self.hit_test(pos);
1513 match hit {
1514 PivotHitResult::VScrollbar => {
1515 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1516 self.scroll_to_vbar(f32::from(pos.y));
1517 }
1518 PivotHitResult::HScrollbar => {
1519 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1520 self.scroll_to_hbar(f32::from(pos.x));
1521 }
1522 PivotHitResult::RowBorder { row } => {
1523 self.resize_drag = Some(PivotResizeDrag::Row {
1524 boundary: row + 1,
1525 start_y: f32::from(pos.y),
1526 start_height: self.row_height,
1527 });
1528 }
1529 PivotHitResult::ColBorder { col } => {
1530 self.resize_drag = Some(PivotResizeDrag::Column {
1531 boundary: col + 1,
1532 start_x: f32::from(pos.x),
1533 start_width: self.value_col_width,
1534 });
1535 }
1536 PivotHitResult::RowHeaderBorder => {
1537 self.resize_drag = Some(PivotResizeDrag::RowHeader {
1538 start_x: f32::from(pos.x),
1539 start_width: self.row_header_width,
1540 });
1541 }
1542 PivotHitResult::RowChevron { row } => {
1543 if let Some(vr) = self.visible_rows.get(row).copied() {
1544 self.toggle_row_group(vr.key);
1545 }
1546 }
1547 PivotHitResult::ColChevron { col, depth } => {
1548 if let Some((node, _)) = self.col_group_run_at(col, depth) {
1549 self.toggle_col_group(node);
1550 } else if let Some(vc) = self.visible_cols.get(col) {
1551 if vc.key != TOTAL_KEY {
1552 self.toggle_col_group(vc.key);
1553 }
1554 }
1555 }
1556 PivotHitResult::Corner => self.cycle_label_sort(),
1557 PivotHitResult::ColHeader { level, col } => {
1558 if level == 0 {
1562 return;
1563 }
1564 if level == self.header_levels() - 1 {
1565 if let Some(vc) = self.visible_cols.get(col).copied() {
1566 self.cycle_sort_by_column(vc.key);
1567 }
1568 } else {
1569 let depth = level - 1;
1570 if let Some((node, _)) = self.col_group_run_at(col, depth) {
1571 if !self.result.col_nodes[node].is_leaf() {
1572 self.toggle_col_group(node);
1573 }
1574 }
1575 }
1576 }
1577 PivotHitResult::RowHeader { row } => {
1578 if self.visible_cols.is_empty() {
1579 return;
1580 }
1581 let last = self.visible_cols.len() - 1;
1582 self.selection = Some((row, 0, row, last));
1583 self.select_anchor = Some((row, 0));
1584 }
1585 PivotHitResult::Cell { row, col } => {
1586 if shift {
1587 let (ar, ac) = self.select_anchor.unwrap_or((row, col));
1588 self.selection = Some(norm_rect(ar, ac, row, col));
1589 } else {
1590 self.selection = Some((row, col, row, col));
1591 self.select_anchor = Some((row, col));
1592 self.is_selecting = true;
1593 }
1594 }
1595 PivotHitResult::None => {
1596 self.selection = None;
1597 self.select_anchor = None;
1598 }
1599 }
1600 }
1601
1602 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, left_down: bool) {
1604 if let Some(drag) = self.resize_drag {
1605 if !left_down {
1606 self.resize_drag = None;
1607 } else {
1608 match drag {
1609 PivotResizeDrag::Row {
1610 boundary,
1611 start_y,
1612 start_height,
1613 } => {
1614 let delta = (f32::from(pos.y) - start_y) / boundary as f32;
1615 self.set_row_height(start_height + delta);
1616 self.hover_hit = Some(PivotHitResult::RowBorder { row: boundary - 1 });
1617 }
1618 PivotResizeDrag::Column {
1619 boundary,
1620 start_x,
1621 start_width,
1622 } => {
1623 let delta = (f32::from(pos.x) - start_x) / boundary as f32;
1624 self.set_column_width(start_width + delta);
1625 self.hover_hit = Some(PivotHitResult::ColBorder { col: boundary - 1 });
1626 }
1627 PivotResizeDrag::RowHeader {
1628 start_x,
1629 start_width,
1630 } => {
1631 let delta = f32::from(pos.x) - start_x;
1632 self.set_row_header_width(start_width + delta);
1633 self.hover_hit = Some(PivotHitResult::RowHeaderBorder);
1634 }
1635 }
1636 return;
1637 }
1638 }
1639 if let Some(axis) = self.scrollbar_drag {
1640 if left_down {
1641 match axis {
1642 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
1643 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
1644 }
1645 return;
1646 }
1647 self.scrollbar_drag = None;
1648 }
1649 let hit = self.hit_test(pos);
1650 self.hover_hit = Some(hit);
1651 if self.is_selecting && left_down {
1652 if let PivotHitResult::Cell { row, col } = hit {
1653 if let Some((ar, ac)) = self.select_anchor {
1654 self.selection = Some(norm_rect(ar, ac, row, col));
1655 }
1656 }
1657 } else if self.is_selecting {
1658 self.is_selecting = false;
1659 }
1660 }
1661
1662 pub fn handle_mouse_up(&mut self) {
1664 self.is_selecting = false;
1665 self.scrollbar_drag = None;
1666 self.resize_drag = None;
1667 }
1668
1669 pub fn apply_scroll_delta(&mut self, dx: f32, dy: f32) {
1671 let (mx, my) = self.max_scroll();
1672 let s = self.scroll_handle.offset();
1673 let nx = (f32::from(s.x) - dx).clamp(0.0, mx);
1674 let ny = (f32::from(s.y) - dy).clamp(0.0, my);
1675 self.scroll_handle.set_offset(Point {
1676 x: px(nx),
1677 y: px(ny),
1678 });
1679 }
1680
1681 pub(crate) fn scroll_to_vbar(&mut self, mouse_y: f32) {
1682 let (_, my) = self.max_scroll();
1683 let hdr = self.header_height();
1684 let (_, rh) = self.scrollbar_reserved();
1685 let track_h = f32::from(self.bounds.size.height) - hdr - rh;
1686 let (_, ch) = self.content_size();
1687 if track_h <= 0.0 || ch <= 0.0 {
1688 return;
1689 }
1690 let thumb_h = ((track_h * (track_h / ch)).max(20.0)).min(track_h);
1691 let range = (track_h - thumb_h).max(0.0);
1692 let rel = (mouse_y - hdr - thumb_h * 0.5).clamp(0.0, range);
1693 let frac = if range > 0.0 { rel / range } else { 0.0 };
1694 let x = self.scroll_handle.offset().x;
1695 self.scroll_handle.set_offset(Point {
1696 x,
1697 y: px(frac * my),
1698 });
1699 }
1700
1701 pub(crate) fn scroll_to_hbar(&mut self, mouse_x: f32) {
1702 let (mx, _) = self.max_scroll();
1703 let (rw, _) = self.scrollbar_reserved();
1704 let track_x = self.row_header_width;
1705 let track_w = f32::from(self.bounds.size.width) - self.row_header_width - rw;
1706 let (cw, _) = self.content_size();
1707 if track_w <= 0.0 || cw <= 0.0 {
1708 return;
1709 }
1710 let thumb_w = ((track_w * (track_w / cw)).max(20.0)).min(track_w);
1711 let range = (track_w - thumb_w).max(0.0);
1712 let rel = (mouse_x - track_x - thumb_w * 0.5).clamp(0.0, range);
1713 let frac = if range > 0.0 { rel / range } else { 0.0 };
1714 let y = self.scroll_handle.offset().y;
1715 self.scroll_handle.set_offset(Point {
1716 x: px(frac * mx),
1717 y,
1718 });
1719 }
1720
1721 pub fn handle_key(&mut self, ks: &gpui::Keystroke, cx: &mut App) {
1723 if self.key_bindings.copy.matches(ks) {
1724 self.copy_selection(false, cx);
1725 return;
1726 }
1727 if self.key_bindings.copy_with_headers.matches(ks) {
1728 self.copy_selection(true, cx);
1729 return;
1730 }
1731 match ks.key.as_str() {
1732 "escape" => {
1733 self.selection = None;
1734 self.select_anchor = None;
1735 self.filter_popover = None;
1736 self.menu = None;
1737 }
1738 "up" => self.move_selection(0, -1),
1739 "down" => self.move_selection(0, 1),
1740 "left" => self.move_selection(-1, 0),
1741 "right" => self.move_selection(1, 0),
1742 _ => {}
1743 }
1744 }
1745
1746 fn move_selection(&mut self, dx: i32, dy: i32) {
1747 let nr = self.visible_rows.len() as i32;
1748 let nc = self.visible_cols.len() as i32;
1749 if nr == 0 || nc == 0 {
1750 return;
1751 }
1752 let (r, c) = match self.selection {
1753 Some((r1, c1, ..)) => (r1 as i32, c1 as i32),
1754 None => (0, 0),
1755 };
1756 let r = (r + dy).clamp(0, nr - 1) as usize;
1757 let c = (c + dx).clamp(0, nc - 1) as usize;
1758 self.selection = Some((r, c, r, c));
1759 self.select_anchor = Some((r, c));
1760 }
1761
1762 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
1769 let text = self.selection_text(with_headers, '\t');
1770 if !text.is_empty() {
1771 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1772 }
1773 }
1774
1775 #[must_use]
1778 pub fn selection_text(&self, with_headers: bool, sep: char) -> String {
1779 use std::fmt::Write as _;
1780 if self.visible_rows.is_empty() || self.visible_cols.is_empty() {
1781 return String::new();
1782 }
1783 let (r1, c1, r2, c2) = self.selection.unwrap_or((
1784 0,
1785 0,
1786 self.visible_rows.len() - 1,
1787 self.visible_cols.len() - 1,
1788 ));
1789 let mut out = String::new();
1790 if with_headers {
1791 for c in c1..=c2 {
1792 out.push(sep);
1793 out.push_str(&self.col_label(&self.visible_cols[c]));
1794 }
1795 out.push('\n');
1796 }
1797 for r in r1..=r2 {
1798 let vr = &self.visible_rows[r];
1799 if with_headers {
1800 let _ = write!(out, "{}{}", " ".repeat(vr.depth), self.row_label(vr));
1801 out.push(sep);
1802 }
1803 for c in c1..=c2 {
1804 if c > c1 {
1805 out.push(sep);
1806 }
1807 let cell = self.cell_value(vr, &self.visible_cols[c]);
1808 if !matches!(cell, CellValue::None) {
1809 out.push_str(&format_cell(cell, &self.value_fmt).0);
1810 }
1811 }
1812 out.push('\n');
1813 }
1814 out
1815 }
1816
1817 #[must_use]
1821 pub fn to_csv(&self) -> String {
1822 let res = &self.result;
1823 let mut out = String::new();
1824 let col_leaves = res.col_leaves();
1825 let want_grand_col = self.config.show_column_grand_total && !col_leaves.is_empty();
1826
1827 let mut header: Vec<String> = if res.row_depth == 0 {
1828 vec![String::new()]
1829 } else {
1830 res.row_field_names.clone()
1831 };
1832 if col_leaves.is_empty() {
1833 header.push(res.value_caption.clone());
1834 } else {
1835 for &leaf in &col_leaves {
1836 header.push(node_path(&res.col_nodes, leaf).join(" / "));
1837 }
1838 }
1839 if want_grand_col {
1840 header.push("Grand Total".into());
1841 }
1842 push_csv_line(&mut out, &header);
1843
1844 let value_cols: Vec<usize> = if col_leaves.is_empty() {
1845 vec![TOTAL_KEY]
1846 } else {
1847 col_leaves
1848 };
1849 let fmt_value = |cell: &CellValue| {
1850 if matches!(cell, CellValue::None) {
1851 String::new()
1852 } else {
1853 format_cell(cell, &self.value_fmt).0
1854 }
1855 };
1856 let emit_line = |out: &mut String, labels: Vec<String>, row_key: usize| {
1857 let mut fields = labels;
1858 for &ck in &value_cols {
1859 fields.push(fmt_value(res.value(row_key, ck)));
1860 }
1861 if want_grand_col {
1862 fields.push(fmt_value(res.value(row_key, TOTAL_KEY)));
1863 }
1864 push_csv_line(out, &fields);
1865 };
1866
1867 let row_leaves = res.row_leaves();
1868 if row_leaves.is_empty() && res.row_depth == 0 && !res.values.is_empty() {
1869 emit_line(&mut out, vec!["Total".into()], TOTAL_KEY);
1870 }
1871 for &leaf in &row_leaves {
1872 let mut labels = node_path(&res.row_nodes, leaf);
1873 while labels.len() < res.row_depth {
1874 labels.push(String::new());
1875 }
1876 emit_line(&mut out, labels, leaf);
1877 }
1878 if self.config.show_row_grand_total && !row_leaves.is_empty() {
1879 let mut labels = vec!["Grand Total".to_owned()];
1880 while labels.len() < res.row_depth.max(1) {
1881 labels.push(String::new());
1882 }
1883 emit_line(&mut out, labels, TOTAL_KEY);
1884 }
1885 out
1886 }
1887}
1888
1889fn default_value_format() -> ResolvedColumnFormat {
1890 crate::config::GridConfig::default().resolve(usize::MAX, ColumnKind::Decimal)
1891}
1892
1893fn norm_rect(r1: usize, c1: usize, r2: usize, c2: usize) -> (usize, usize, usize, usize) {
1894 (r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2))
1895}
1896
1897fn push_csv_line(out: &mut String, fields: &[String]) {
1898 for (i, f) in fields.iter().enumerate() {
1899 if i > 0 {
1900 out.push(',');
1901 }
1902 if f.contains(',') || f.contains('"') || f.contains('\n') {
1903 out.push('"');
1904 out.push_str(&f.replace('"', "\"\""));
1905 out.push('"');
1906 } else {
1907 out.push_str(f);
1908 }
1909 }
1910 out.push('\n');
1911}
1912
1913pub(crate) fn node_path(nodes: &[PivotNode], node: usize) -> Vec<String> {
1915 let mut path = Vec::new();
1916 let mut current = Some(node);
1917 while let Some(id) = current {
1918 path.push(nodes[id].label.clone());
1919 current = nodes[id].parent;
1920 }
1921 path.reverse();
1922 path
1923}
1924
1925pub(crate) fn path_components(
1928 nodes: &[PivotNode],
1929 fields: &[usize],
1930 columns: &[Column],
1931 blank_label: &str,
1932 key: usize,
1933) -> Vec<PivotPathComponent> {
1934 if key == TOTAL_KEY || key >= nodes.len() {
1935 return Vec::new();
1936 }
1937 let mut chain = Vec::new();
1938 let mut current = Some(key);
1939 while let Some(id) = current {
1940 chain.push(id);
1941 current = nodes[id].parent;
1942 }
1943 chain.reverse();
1944 chain
1945 .into_iter()
1946 .map(|id| {
1947 let node = &nodes[id];
1948 let field_index = fields.get(node.depth).copied().unwrap_or(usize::MAX);
1949 PivotPathComponent {
1950 field_index,
1951 field_name: columns
1952 .get(field_index)
1953 .map(|c| c.name.clone())
1954 .unwrap_or_default(),
1955 label: node.label.clone(),
1956 group_value: node.sort_key.clone(),
1957 is_blank: node.label == blank_label,
1958 }
1959 })
1960 .collect()
1961}
1962
1963pub(crate) fn rows_matching_path(
1967 rows: &[Vec<CellValue>],
1968 formats: &[ResolvedColumnFormat],
1969 blank_label: &str,
1970 base: &[usize],
1971 constraints: &[(usize, String)],
1972) -> Vec<usize> {
1973 if constraints.is_empty() {
1974 return base.to_vec();
1975 }
1976 base.iter()
1977 .copied()
1978 .filter(|&r| {
1979 constraints.iter().all(|(field, label)| {
1980 let cell = rows
1981 .get(r)
1982 .and_then(|row| row.get(*field))
1983 .unwrap_or(&CellValue::None);
1984 let cell_label = if matches!(cell, CellValue::None) {
1985 blank_label.to_owned()
1986 } else {
1987 format_cell(cell, &formats[*field]).0
1988 };
1989 cell_label == *label
1990 })
1991 })
1992 .collect()
1993}
1994
1995pub(crate) fn drill_filter_set(label: &str, blank_label: &str) -> HashSet<String> {
2000 if label == blank_label {
2001 HashSet::from([String::new(), label.to_owned()])
2002 } else {
2003 HashSet::from([label.to_owned()])
2004 }
2005}
2006
2007fn all_group_paths(nodes: &[PivotNode]) -> HashSet<Vec<String>> {
2009 nodes
2010 .iter()
2011 .enumerate()
2012 .filter(|(_, n)| !n.is_leaf())
2013 .map(|(id, _)| node_path(nodes, id))
2014 .collect()
2015}
2016
2017pub(crate) fn flatten_rows(
2019 result: &PivotResult,
2020 collapsed: &HashSet<Vec<String>>,
2021 config: &PivotConfig,
2022) -> Vec<VisibleRow> {
2023 let mut out = Vec::new();
2024 if result.row_depth == 0 {
2025 if !result.values.is_empty() {
2027 out.push(VisibleRow {
2028 key: TOTAL_KEY,
2029 depth: 0,
2030 kind: VisibleRowKind::Leaf,
2031 zebra: false,
2032 });
2033 }
2034 return out;
2035 }
2036 if config.flat_rows {
2037 let mut zebra = false;
2044 for leaf in result.row_leaves() {
2045 out.push(VisibleRow {
2046 key: leaf,
2047 depth: result.row_nodes[leaf].depth,
2048 kind: VisibleRowKind::Leaf,
2049 zebra,
2050 });
2051 zebra = !zebra;
2052 }
2053 if config.show_row_grand_total && !out.is_empty() {
2054 out.push(VisibleRow {
2055 key: TOTAL_KEY,
2056 depth: 0,
2057 kind: VisibleRowKind::GrandTotal,
2058 zebra: false,
2059 });
2060 }
2061 return out;
2062 }
2063 fn walk(
2064 result: &PivotResult,
2065 collapsed: &HashSet<Vec<String>>,
2066 id: usize,
2067 path: &mut Vec<String>,
2068 zebra: &mut bool,
2069 out: &mut Vec<VisibleRow>,
2070 ) {
2071 let node = &result.row_nodes[id];
2072 path.push(node.label.clone());
2073 if node.is_leaf() {
2074 out.push(VisibleRow {
2075 key: id,
2076 depth: node.depth,
2077 kind: VisibleRowKind::Leaf,
2078 zebra: *zebra,
2079 });
2080 *zebra = !*zebra;
2081 } else if collapsed.contains(path) {
2082 *zebra = false;
2083 out.push(VisibleRow {
2084 key: id,
2085 depth: node.depth,
2086 kind: VisibleRowKind::GroupHeader { expanded: false },
2087 zebra: false,
2088 });
2089 } else {
2090 *zebra = false;
2092 out.push(VisibleRow {
2093 key: id,
2094 depth: node.depth,
2095 kind: VisibleRowKind::GroupHeader { expanded: true },
2096 zebra: false,
2097 });
2098 for &child in &node.children {
2099 walk(result, collapsed, child, path, zebra, out);
2100 }
2101 *zebra = false;
2102 }
2103 path.pop();
2104 }
2105 let mut path = Vec::new();
2106 let mut zebra = false;
2107 for &root in &result.row_roots {
2108 walk(result, collapsed, root, &mut path, &mut zebra, &mut out);
2109 }
2110 if config.show_row_grand_total && !out.is_empty() {
2111 out.push(VisibleRow {
2112 key: TOTAL_KEY,
2113 depth: 0,
2114 kind: VisibleRowKind::GrandTotal,
2115 zebra: false,
2116 });
2117 }
2118 out
2119}
2120
2121pub(crate) fn flatten_cols(
2123 result: &PivotResult,
2124 collapsed: &HashSet<Vec<String>>,
2125 config: &PivotConfig,
2126) -> Vec<VisibleCol> {
2127 let mut out = Vec::new();
2128 if result.col_depth == 0 {
2129 if !result.values.is_empty() {
2130 out.push(VisibleCol {
2131 key: TOTAL_KEY,
2132 depth: 0,
2133 kind: VisibleColKind::Leaf,
2134 });
2135 }
2136 return out;
2137 }
2138 fn walk(
2139 result: &PivotResult,
2140 collapsed: &HashSet<Vec<String>>,
2141 config: &PivotConfig,
2142 id: usize,
2143 path: &mut Vec<String>,
2144 out: &mut Vec<VisibleCol>,
2145 ) {
2146 let node = &result.col_nodes[id];
2147 path.push(node.label.clone());
2148 if node.is_leaf() {
2149 out.push(VisibleCol {
2150 key: id,
2151 depth: node.depth,
2152 kind: VisibleColKind::Leaf,
2153 });
2154 } else if collapsed.contains(path) {
2155 out.push(VisibleCol {
2156 key: id,
2157 depth: node.depth,
2158 kind: VisibleColKind::Collapsed,
2159 });
2160 } else {
2161 for &child in &node.children {
2162 walk(result, collapsed, config, child, path, out);
2163 }
2164 if config.show_column_subtotals {
2165 out.push(VisibleCol {
2166 key: id,
2167 depth: node.depth,
2168 kind: VisibleColKind::Subtotal,
2169 });
2170 }
2171 }
2172 path.pop();
2173 }
2174 let mut path = Vec::new();
2175 for &root in &result.col_roots {
2176 walk(result, collapsed, config, root, &mut path, &mut out);
2177 }
2178 if config.show_column_grand_total && !out.is_empty() {
2179 out.push(VisibleCol {
2180 key: TOTAL_KEY,
2181 depth: 0,
2182 kind: VisibleColKind::GrandTotal,
2183 });
2184 }
2185 out
2186}
2187
2188fn sort_axis_by_key(nodes: &mut [PivotNode], roots: &mut [usize], descending: bool) {
2190 let keys: Vec<CellValue> = nodes.iter().map(|n| n.sort_key.clone()).collect();
2191 sort_axis_by(nodes, roots, &keys, descending);
2192}
2193
2194fn sort_axis_by(
2196 nodes: &mut [PivotNode],
2197 roots: &mut [usize],
2198 keys: &[CellValue],
2199 descending: bool,
2200) {
2201 let cmp = |a: &usize, b: &usize| {
2202 let ord = compare_cells(&keys[*a], &keys[*b]);
2203 if descending {
2204 ord.reverse()
2205 } else {
2206 ord
2207 }
2208 };
2209 roots.sort_by(cmp);
2210 for node in nodes.iter_mut() {
2211 let mut children = std::mem::take(&mut node.children);
2212 children.sort_by(cmp);
2213 node.children = children;
2214 }
2215}
2216
2217#[cfg(test)]
2218#[allow(clippy::unwrap_used)]
2219mod tests {
2220 use super::*;
2221 use crate::config::GridConfig;
2222 use crate::data::ColumnKind;
2223 use CellValue::{Decimal, Integer, Text};
2224
2225 fn fixture_result(config: &PivotConfig) -> PivotResult {
2226 let columns = vec![
2227 Column::new("region", ColumnKind::Text, 100.0),
2228 Column::new("product", ColumnKind::Text, 100.0),
2229 Column::new("year", ColumnKind::Integer, 80.0),
2230 Column::new("amount", ColumnKind::Decimal, 100.0),
2231 ];
2232 let r = |region: &str, product: &str, year: i64, amount: f64| {
2233 vec![
2234 Text(region.into()),
2235 Text(product.into()),
2236 Integer(year),
2237 Decimal(amount),
2238 ]
2239 };
2240 let rows = vec![
2241 r("Europe", "Widget", 2023, 10.0),
2242 r("Europe", "Widget", 2024, 20.0),
2243 r("Europe", "Gadget", 2023, 5.0),
2244 r("Asia", "Widget", 2023, 7.0),
2245 r("Asia", "Gadget", 2024, 3.0),
2246 ];
2247 let formats = GridConfig::default().resolve_all(&columns);
2248 let all: Vec<usize> = (0..rows.len()).collect();
2249 compute_pivot(&columns, &rows, &all, config, &formats)
2250 }
2251
2252 fn two_level_config() -> PivotConfig {
2253 PivotConfig {
2254 row_fields: vec![0, 1],
2255 column_fields: vec![2],
2256 value_field: Some(3),
2257 ..PivotConfig::default()
2258 }
2259 }
2260
2261 #[test]
2262 fn flatten_rows_expanded_lists_headers_and_leaves() {
2263 let cfg = two_level_config();
2264 let result = fixture_result(&cfg);
2265 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2266 assert_eq!(rows.len(), 7);
2268 assert_eq!(rows[0].kind, VisibleRowKind::GroupHeader { expanded: true });
2269 assert_eq!(rows[0].depth, 0);
2270 assert_eq!(rows[1].kind, VisibleRowKind::Leaf);
2271 assert_eq!(rows[1].depth, 1);
2272 assert_eq!(rows[6].kind, VisibleRowKind::GrandTotal);
2273 }
2274
2275 #[test]
2276 fn flatten_rows_collapse_hides_children() {
2277 let cfg = two_level_config();
2278 let result = fixture_result(&cfg);
2279 let mut collapsed = HashSet::new();
2280 collapsed.insert(vec!["Asia".to_owned()]);
2281 let rows = flatten_rows(&result, &collapsed, &cfg);
2282 assert_eq!(rows.len(), 5);
2284 assert_eq!(
2285 rows[0].kind,
2286 VisibleRowKind::GroupHeader { expanded: false }
2287 );
2288 assert_eq!(rows[1].kind, VisibleRowKind::GroupHeader { expanded: true });
2289 }
2290
2291 #[test]
2292 fn flatten_rows_without_grand_total() {
2293 let mut cfg = two_level_config();
2294 cfg.show_row_grand_total = false;
2295 let result = fixture_result(&cfg);
2296 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2297 assert!(rows.iter().all(|r| r.kind != VisibleRowKind::GrandTotal));
2298 }
2299
2300 #[test]
2301 fn flatten_rows_flat_lists_leaf_combinations_without_hierarchy() {
2302 let mut cfg = two_level_config();
2303 cfg.flat_rows = true;
2304 let result = fixture_result(&cfg);
2305 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2306 assert_eq!(rows.len(), 5);
2308 assert!(rows[..4].iter().all(|r| r.kind == VisibleRowKind::Leaf));
2309 assert!(rows
2310 .iter()
2311 .all(|r| !matches!(r.kind, VisibleRowKind::GroupHeader { .. })));
2312 assert_eq!(rows[4].kind, VisibleRowKind::GrandTotal);
2313 let paths: Vec<Vec<String>> = rows[..4]
2316 .iter()
2317 .map(|r| node_path(&result.row_nodes, r.key))
2318 .collect();
2319 assert!(paths.iter().all(|p| p.len() == 2));
2320 assert_eq!(paths[0], vec!["Asia".to_owned(), "Gadget".to_owned()]);
2321 assert_eq!(paths[3], vec!["Europe".to_owned(), "Widget".to_owned()]);
2322 }
2323
2324 #[test]
2325 fn flatten_rows_flat_respects_grand_total_toggle() {
2326 let mut cfg = two_level_config();
2327 cfg.flat_rows = true;
2328 cfg.show_row_grand_total = false;
2329 let result = fixture_result(&cfg);
2330 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2331 assert_eq!(rows.len(), 4);
2332 assert!(rows.iter().all(|r| r.kind == VisibleRowKind::Leaf));
2333 }
2334
2335 #[test]
2336 fn flatten_cols_leaves_plus_grand_total() {
2337 let cfg = two_level_config();
2338 let result = fixture_result(&cfg);
2339 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2340 assert_eq!(cols.len(), 3);
2342 assert_eq!(cols[0].kind, VisibleColKind::Leaf);
2343 assert_eq!(cols[2].kind, VisibleColKind::GrandTotal);
2344 }
2345
2346 #[test]
2347 fn flatten_cols_collapsed_group_is_single_column() {
2348 let mut cfg = two_level_config();
2349 cfg.row_fields = vec![0];
2351 cfg.column_fields = vec![2, 1];
2352 let result = fixture_result(&cfg);
2353 let first_year = result.col_roots[0];
2356 let mut collapsed = HashSet::new();
2357 collapsed.insert(vec![result.col_nodes[first_year].label.clone()]);
2358 let cols = flatten_cols(&result, &collapsed, &cfg);
2359 assert_eq!(cols[0].kind, VisibleColKind::Collapsed);
2361 assert!(cols.len() >= 3);
2362 }
2363
2364 #[test]
2365 fn flatten_cols_subtotal_columns_follow_expanded_groups() {
2366 let mut cfg = two_level_config();
2367 cfg.row_fields = vec![0];
2368 cfg.column_fields = vec![2, 1];
2369 cfg.show_column_subtotals = true;
2370 let result = fixture_result(&cfg);
2371 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2372 let subtotal_count = cols
2373 .iter()
2374 .filter(|c| c.kind == VisibleColKind::Subtotal)
2375 .count();
2376 assert_eq!(subtotal_count, 2); let first_sub = cols
2379 .iter()
2380 .position(|c| c.kind == VisibleColKind::Subtotal)
2381 .unwrap();
2382 assert!(first_sub > 0);
2383 assert_eq!(cols[first_sub - 1].kind, VisibleColKind::Leaf);
2384 }
2385
2386 #[test]
2387 fn flatten_cols_no_column_fields_yields_single_value_column() {
2388 let mut cfg = two_level_config();
2389 cfg.column_fields = vec![];
2390 let result = fixture_result(&cfg);
2391 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2392 assert_eq!(cols.len(), 1);
2393 assert_eq!(cols[0].key, TOTAL_KEY);
2394 }
2395
2396 #[test]
2397 fn flatten_empty_result_is_empty() {
2398 let cfg = PivotConfig::default();
2399 let result = PivotResult::default();
2400 assert!(flatten_rows(&result, &HashSet::new(), &cfg).is_empty());
2401 assert!(flatten_cols(&result, &HashSet::new(), &cfg).is_empty());
2402 }
2403
2404 #[test]
2405 fn sort_axis_descending_reverses_roots_and_children() {
2406 let cfg = two_level_config();
2407 let mut result = fixture_result(&cfg);
2408 let labels = |result: &PivotResult| -> Vec<String> {
2409 result
2410 .row_roots
2411 .iter()
2412 .map(|&r| result.row_nodes[r].label.clone())
2413 .collect()
2414 };
2415 assert_eq!(labels(&result), vec!["Asia", "Europe"]);
2416 let mut roots = result.row_roots.clone();
2417 sort_axis_by_key(&mut result.row_nodes, &mut roots, true);
2418 result.row_roots = roots;
2419 assert_eq!(labels(&result), vec!["Europe", "Asia"]);
2420 let asia = result.row_roots[1];
2421 let child_labels: Vec<&str> = result.row_nodes[asia]
2422 .children
2423 .iter()
2424 .map(|&c| result.row_nodes[c].label.as_str())
2425 .collect();
2426 assert_eq!(child_labels, vec!["Widget", "Gadget"]);
2427 }
2428
2429 #[test]
2430 fn sort_axis_by_values_orders_missing_first_ascending() {
2431 let cfg = two_level_config();
2432 let mut result = fixture_result(&cfg);
2433 let y2024 = result
2435 .col_roots
2436 .iter()
2437 .copied()
2438 .find(|&c| result.col_nodes[c].sort_key == Integer(2024))
2439 .unwrap();
2440 let keys: Vec<CellValue> = (0..result.row_nodes.len())
2441 .map(|id| {
2442 result
2443 .values
2444 .get(&(id, y2024))
2445 .cloned()
2446 .unwrap_or(CellValue::None)
2447 })
2448 .collect();
2449 let mut roots = result.row_roots.clone();
2450 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, false);
2451 let labels: Vec<&str> = roots
2452 .iter()
2453 .map(|&r| result.row_nodes[r].label.as_str())
2454 .collect();
2455 assert_eq!(labels, vec!["Asia", "Europe"]);
2456 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, true);
2457 let labels: Vec<&str> = roots
2458 .iter()
2459 .map(|&r| result.row_nodes[r].label.as_str())
2460 .collect();
2461 assert_eq!(labels, vec!["Europe", "Asia"]);
2462 }
2463
2464 #[test]
2465 fn node_path_walks_to_root() {
2466 let cfg = two_level_config();
2467 let result = fixture_result(&cfg);
2468 let europe = result
2469 .row_nodes
2470 .iter()
2471 .position(|n| n.label == "Europe")
2472 .unwrap();
2473 let widget = result.row_nodes[europe]
2474 .children
2475 .iter()
2476 .copied()
2477 .find(|&c| result.row_nodes[c].label == "Widget")
2478 .unwrap();
2479 assert_eq!(
2480 node_path(&result.row_nodes, widget),
2481 vec!["Europe".to_owned(), "Widget".to_owned()]
2482 );
2483 }
2484
2485 #[test]
2486 fn all_group_paths_lists_only_non_leaves() {
2487 let cfg = two_level_config();
2488 let result = fixture_result(&cfg);
2489 let paths = all_group_paths(&result.row_nodes);
2490 assert_eq!(paths.len(), 2); assert!(paths.contains(&vec!["Asia".to_owned()]));
2492 }
2493
2494 #[test]
2495 fn csv_line_quotes_fields_with_commas_and_quotes() {
2496 let mut out = String::new();
2497 push_csv_line(
2498 &mut out,
2499 &["a,b".to_owned(), "plain".to_owned(), "q\"q".to_owned()],
2500 );
2501 assert_eq!(out, "\"a,b\",plain,\"q\"\"q\"\n");
2502 }
2503
2504 fn fixture_columns() -> Vec<Column> {
2505 vec![
2506 Column::new("region", ColumnKind::Text, 100.0),
2507 Column::new("product", ColumnKind::Text, 100.0),
2508 Column::new("year", ColumnKind::Integer, 80.0),
2509 Column::new("amount", ColumnKind::Decimal, 100.0),
2510 ]
2511 }
2512
2513 fn fixture_rows() -> Vec<Vec<CellValue>> {
2514 let r = |region: &str, product: &str, year: i64, amount: f64| {
2515 vec![
2516 Text(region.into()),
2517 Text(product.into()),
2518 Integer(year),
2519 Decimal(amount),
2520 ]
2521 };
2522 vec![
2523 r("Europe", "Widget", 2023, 10.0),
2524 r("Europe", "Widget", 2024, 20.0),
2525 r("Europe", "Gadget", 2023, 5.0),
2526 r("Asia", "Widget", 2023, 7.0),
2527 r("Asia", "Gadget", 2024, 3.0),
2528 ]
2529 }
2530
2531 #[test]
2532 fn path_components_walk_root_to_leaf_with_field_metadata() {
2533 let cfg = two_level_config();
2534 let result = fixture_result(&cfg);
2535 let columns = fixture_columns();
2536 let europe = result
2537 .row_nodes
2538 .iter()
2539 .position(|n| n.label == "Europe")
2540 .unwrap();
2541 let widget = result.row_nodes[europe]
2542 .children
2543 .iter()
2544 .copied()
2545 .find(|&c| result.row_nodes[c].label == "Widget")
2546 .unwrap();
2547 let path = path_components(
2548 &result.row_nodes,
2549 &cfg.row_fields,
2550 &columns,
2551 "(blank)",
2552 widget,
2553 );
2554 assert_eq!(path.len(), 2);
2555 assert_eq!(path[0].field_name, "region");
2556 assert_eq!(path[0].label, "Europe");
2557 assert_eq!(path[0].field_index, 0);
2558 assert_eq!(path[1].field_name, "product");
2559 assert_eq!(path[1].label, "Widget");
2560 assert!(!path[1].is_blank);
2561 assert!(path_components(
2563 &result.row_nodes,
2564 &cfg.row_fields,
2565 &columns,
2566 "(blank)",
2567 TOTAL_KEY
2568 )
2569 .is_empty());
2570 }
2571
2572 #[test]
2573 fn rows_matching_path_selects_only_driving_rows() {
2574 let rows = fixture_rows();
2575 let columns = fixture_columns();
2576 let formats = GridConfig::default().resolve_all(&columns);
2577 let base: Vec<usize> = (0..rows.len()).collect();
2578 let constraints = vec![(0, "Europe".to_owned()), (1, "Widget".to_owned())];
2580 assert_eq!(
2581 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2582 vec![0, 1]
2583 );
2584 assert_eq!(
2586 rows_matching_path(&rows, &formats, "(blank)", &base, &[]),
2587 base
2588 );
2589 assert_eq!(
2591 rows_matching_path(&rows, &formats, "(blank)", &[1, 2, 3], &constraints),
2592 vec![1]
2593 );
2594 }
2595
2596 #[test]
2597 fn rows_matching_path_buckets_nulls_under_blank_label() {
2598 let mut rows = fixture_rows();
2599 rows.push(vec![
2600 CellValue::None,
2601 Text("Widget".into()),
2602 Integer(2023),
2603 Decimal(2.0),
2604 ]);
2605 let columns = fixture_columns();
2606 let formats = GridConfig::default().resolve_all(&columns);
2607 let base: Vec<usize> = (0..rows.len()).collect();
2608 let constraints = vec![(0, "(blank)".to_owned())];
2609 assert_eq!(
2610 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2611 vec![5]
2612 );
2613 }
2614
2615 #[test]
2616 fn drill_filter_set_maps_blank_to_empty_formatted_value() {
2617 let set = drill_filter_set("(blank)", "(blank)");
2618 assert!(set.contains(""));
2619 assert!(set.contains("(blank)"));
2620 let set = drill_filter_set("Europe", "(blank)");
2621 assert_eq!(set, HashSet::from(["Europe".to_owned()]));
2622 }
2623}