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