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