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: crate::grid::paint::default_char_width(13.0),
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 if config.flat_rows {
1981 let mut zebra = false;
1988 for leaf in result.row_leaves() {
1989 out.push(VisibleRow {
1990 key: leaf,
1991 depth: result.row_nodes[leaf].depth,
1992 kind: VisibleRowKind::Leaf,
1993 zebra,
1994 });
1995 zebra = !zebra;
1996 }
1997 if config.show_row_grand_total && !out.is_empty() {
1998 out.push(VisibleRow {
1999 key: TOTAL_KEY,
2000 depth: 0,
2001 kind: VisibleRowKind::GrandTotal,
2002 zebra: false,
2003 });
2004 }
2005 return out;
2006 }
2007 fn walk(
2008 result: &PivotResult,
2009 collapsed: &HashSet<Vec<String>>,
2010 id: usize,
2011 path: &mut Vec<String>,
2012 zebra: &mut bool,
2013 out: &mut Vec<VisibleRow>,
2014 ) {
2015 let node = &result.row_nodes[id];
2016 path.push(node.label.clone());
2017 if node.is_leaf() {
2018 out.push(VisibleRow {
2019 key: id,
2020 depth: node.depth,
2021 kind: VisibleRowKind::Leaf,
2022 zebra: *zebra,
2023 });
2024 *zebra = !*zebra;
2025 } else if collapsed.contains(path) {
2026 *zebra = false;
2027 out.push(VisibleRow {
2028 key: id,
2029 depth: node.depth,
2030 kind: VisibleRowKind::GroupHeader { expanded: false },
2031 zebra: false,
2032 });
2033 } else {
2034 *zebra = false;
2036 out.push(VisibleRow {
2037 key: id,
2038 depth: node.depth,
2039 kind: VisibleRowKind::GroupHeader { expanded: true },
2040 zebra: false,
2041 });
2042 for &child in &node.children {
2043 walk(result, collapsed, child, path, zebra, out);
2044 }
2045 *zebra = false;
2046 }
2047 path.pop();
2048 }
2049 let mut path = Vec::new();
2050 let mut zebra = false;
2051 for &root in &result.row_roots {
2052 walk(result, collapsed, root, &mut path, &mut zebra, &mut out);
2053 }
2054 if config.show_row_grand_total && !out.is_empty() {
2055 out.push(VisibleRow {
2056 key: TOTAL_KEY,
2057 depth: 0,
2058 kind: VisibleRowKind::GrandTotal,
2059 zebra: false,
2060 });
2061 }
2062 out
2063}
2064
2065pub(crate) fn flatten_cols(
2067 result: &PivotResult,
2068 collapsed: &HashSet<Vec<String>>,
2069 config: &PivotConfig,
2070) -> Vec<VisibleCol> {
2071 let mut out = Vec::new();
2072 if result.col_depth == 0 {
2073 if !result.values.is_empty() {
2074 out.push(VisibleCol {
2075 key: TOTAL_KEY,
2076 depth: 0,
2077 kind: VisibleColKind::Leaf,
2078 });
2079 }
2080 return out;
2081 }
2082 fn walk(
2083 result: &PivotResult,
2084 collapsed: &HashSet<Vec<String>>,
2085 config: &PivotConfig,
2086 id: usize,
2087 path: &mut Vec<String>,
2088 out: &mut Vec<VisibleCol>,
2089 ) {
2090 let node = &result.col_nodes[id];
2091 path.push(node.label.clone());
2092 if node.is_leaf() {
2093 out.push(VisibleCol {
2094 key: id,
2095 depth: node.depth,
2096 kind: VisibleColKind::Leaf,
2097 });
2098 } else if collapsed.contains(path) {
2099 out.push(VisibleCol {
2100 key: id,
2101 depth: node.depth,
2102 kind: VisibleColKind::Collapsed,
2103 });
2104 } else {
2105 for &child in &node.children {
2106 walk(result, collapsed, config, child, path, out);
2107 }
2108 if config.show_column_subtotals {
2109 out.push(VisibleCol {
2110 key: id,
2111 depth: node.depth,
2112 kind: VisibleColKind::Subtotal,
2113 });
2114 }
2115 }
2116 path.pop();
2117 }
2118 let mut path = Vec::new();
2119 for &root in &result.col_roots {
2120 walk(result, collapsed, config, root, &mut path, &mut out);
2121 }
2122 if config.show_column_grand_total && !out.is_empty() {
2123 out.push(VisibleCol {
2124 key: TOTAL_KEY,
2125 depth: 0,
2126 kind: VisibleColKind::GrandTotal,
2127 });
2128 }
2129 out
2130}
2131
2132fn sort_axis_by_key(nodes: &mut [PivotNode], roots: &mut [usize], descending: bool) {
2134 let keys: Vec<CellValue> = nodes.iter().map(|n| n.sort_key.clone()).collect();
2135 sort_axis_by(nodes, roots, &keys, descending);
2136}
2137
2138fn sort_axis_by(
2140 nodes: &mut [PivotNode],
2141 roots: &mut [usize],
2142 keys: &[CellValue],
2143 descending: bool,
2144) {
2145 let cmp = |a: &usize, b: &usize| {
2146 let ord = compare_cells(&keys[*a], &keys[*b]);
2147 if descending {
2148 ord.reverse()
2149 } else {
2150 ord
2151 }
2152 };
2153 roots.sort_by(cmp);
2154 for node in nodes.iter_mut() {
2155 let mut children = std::mem::take(&mut node.children);
2156 children.sort_by(cmp);
2157 node.children = children;
2158 }
2159}
2160
2161#[cfg(test)]
2162#[allow(clippy::unwrap_used)]
2163mod tests {
2164 use super::*;
2165 use crate::config::GridConfig;
2166 use crate::data::ColumnKind;
2167 use CellValue::{Decimal, Integer, Text};
2168
2169 fn fixture_result(config: &PivotConfig) -> PivotResult {
2170 let columns = vec![
2171 Column::new("region", ColumnKind::Text, 100.0),
2172 Column::new("product", ColumnKind::Text, 100.0),
2173 Column::new("year", ColumnKind::Integer, 80.0),
2174 Column::new("amount", ColumnKind::Decimal, 100.0),
2175 ];
2176 let r = |region: &str, product: &str, year: i64, amount: f64| {
2177 vec![
2178 Text(region.into()),
2179 Text(product.into()),
2180 Integer(year),
2181 Decimal(amount),
2182 ]
2183 };
2184 let rows = vec![
2185 r("Europe", "Widget", 2023, 10.0),
2186 r("Europe", "Widget", 2024, 20.0),
2187 r("Europe", "Gadget", 2023, 5.0),
2188 r("Asia", "Widget", 2023, 7.0),
2189 r("Asia", "Gadget", 2024, 3.0),
2190 ];
2191 let formats = GridConfig::default().resolve_all(&columns);
2192 let all: Vec<usize> = (0..rows.len()).collect();
2193 compute_pivot(&columns, &rows, &all, config, &formats)
2194 }
2195
2196 fn two_level_config() -> PivotConfig {
2197 PivotConfig {
2198 row_fields: vec![0, 1],
2199 column_fields: vec![2],
2200 value_field: Some(3),
2201 ..PivotConfig::default()
2202 }
2203 }
2204
2205 #[test]
2206 fn flatten_rows_expanded_lists_headers_and_leaves() {
2207 let cfg = two_level_config();
2208 let result = fixture_result(&cfg);
2209 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2210 assert_eq!(rows.len(), 7);
2212 assert_eq!(rows[0].kind, VisibleRowKind::GroupHeader { expanded: true });
2213 assert_eq!(rows[0].depth, 0);
2214 assert_eq!(rows[1].kind, VisibleRowKind::Leaf);
2215 assert_eq!(rows[1].depth, 1);
2216 assert_eq!(rows[6].kind, VisibleRowKind::GrandTotal);
2217 }
2218
2219 #[test]
2220 fn flatten_rows_collapse_hides_children() {
2221 let cfg = two_level_config();
2222 let result = fixture_result(&cfg);
2223 let mut collapsed = HashSet::new();
2224 collapsed.insert(vec!["Asia".to_owned()]);
2225 let rows = flatten_rows(&result, &collapsed, &cfg);
2226 assert_eq!(rows.len(), 5);
2228 assert_eq!(
2229 rows[0].kind,
2230 VisibleRowKind::GroupHeader { expanded: false }
2231 );
2232 assert_eq!(rows[1].kind, VisibleRowKind::GroupHeader { expanded: true });
2233 }
2234
2235 #[test]
2236 fn flatten_rows_without_grand_total() {
2237 let mut cfg = two_level_config();
2238 cfg.show_row_grand_total = false;
2239 let result = fixture_result(&cfg);
2240 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2241 assert!(rows.iter().all(|r| r.kind != VisibleRowKind::GrandTotal));
2242 }
2243
2244 #[test]
2245 fn flatten_rows_flat_lists_leaf_combinations_without_hierarchy() {
2246 let mut cfg = two_level_config();
2247 cfg.flat_rows = true;
2248 let result = fixture_result(&cfg);
2249 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2250 assert_eq!(rows.len(), 5);
2252 assert!(rows[..4].iter().all(|r| r.kind == VisibleRowKind::Leaf));
2253 assert!(rows
2254 .iter()
2255 .all(|r| !matches!(r.kind, VisibleRowKind::GroupHeader { .. })));
2256 assert_eq!(rows[4].kind, VisibleRowKind::GrandTotal);
2257 let paths: Vec<Vec<String>> = rows[..4]
2260 .iter()
2261 .map(|r| node_path(&result.row_nodes, r.key))
2262 .collect();
2263 assert!(paths.iter().all(|p| p.len() == 2));
2264 assert_eq!(paths[0], vec!["Asia".to_owned(), "Gadget".to_owned()]);
2265 assert_eq!(paths[3], vec!["Europe".to_owned(), "Widget".to_owned()]);
2266 }
2267
2268 #[test]
2269 fn flatten_rows_flat_respects_grand_total_toggle() {
2270 let mut cfg = two_level_config();
2271 cfg.flat_rows = true;
2272 cfg.show_row_grand_total = false;
2273 let result = fixture_result(&cfg);
2274 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2275 assert_eq!(rows.len(), 4);
2276 assert!(rows.iter().all(|r| r.kind == VisibleRowKind::Leaf));
2277 }
2278
2279 #[test]
2280 fn flatten_cols_leaves_plus_grand_total() {
2281 let cfg = two_level_config();
2282 let result = fixture_result(&cfg);
2283 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2284 assert_eq!(cols.len(), 3);
2286 assert_eq!(cols[0].kind, VisibleColKind::Leaf);
2287 assert_eq!(cols[2].kind, VisibleColKind::GrandTotal);
2288 }
2289
2290 #[test]
2291 fn flatten_cols_collapsed_group_is_single_column() {
2292 let mut cfg = two_level_config();
2293 cfg.row_fields = vec![0];
2295 cfg.column_fields = vec![2, 1];
2296 let result = fixture_result(&cfg);
2297 let first_year = result.col_roots[0];
2300 let mut collapsed = HashSet::new();
2301 collapsed.insert(vec![result.col_nodes[first_year].label.clone()]);
2302 let cols = flatten_cols(&result, &collapsed, &cfg);
2303 assert_eq!(cols[0].kind, VisibleColKind::Collapsed);
2305 assert!(cols.len() >= 3);
2306 }
2307
2308 #[test]
2309 fn flatten_cols_subtotal_columns_follow_expanded_groups() {
2310 let mut cfg = two_level_config();
2311 cfg.row_fields = vec![0];
2312 cfg.column_fields = vec![2, 1];
2313 cfg.show_column_subtotals = true;
2314 let result = fixture_result(&cfg);
2315 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2316 let subtotal_count = cols
2317 .iter()
2318 .filter(|c| c.kind == VisibleColKind::Subtotal)
2319 .count();
2320 assert_eq!(subtotal_count, 2); let first_sub = cols
2323 .iter()
2324 .position(|c| c.kind == VisibleColKind::Subtotal)
2325 .unwrap();
2326 assert!(first_sub > 0);
2327 assert_eq!(cols[first_sub - 1].kind, VisibleColKind::Leaf);
2328 }
2329
2330 #[test]
2331 fn flatten_cols_no_column_fields_yields_single_value_column() {
2332 let mut cfg = two_level_config();
2333 cfg.column_fields = vec![];
2334 let result = fixture_result(&cfg);
2335 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2336 assert_eq!(cols.len(), 1);
2337 assert_eq!(cols[0].key, TOTAL_KEY);
2338 }
2339
2340 #[test]
2341 fn flatten_empty_result_is_empty() {
2342 let cfg = PivotConfig::default();
2343 let result = PivotResult::default();
2344 assert!(flatten_rows(&result, &HashSet::new(), &cfg).is_empty());
2345 assert!(flatten_cols(&result, &HashSet::new(), &cfg).is_empty());
2346 }
2347
2348 #[test]
2349 fn sort_axis_descending_reverses_roots_and_children() {
2350 let cfg = two_level_config();
2351 let mut result = fixture_result(&cfg);
2352 let labels = |result: &PivotResult| -> Vec<String> {
2353 result
2354 .row_roots
2355 .iter()
2356 .map(|&r| result.row_nodes[r].label.clone())
2357 .collect()
2358 };
2359 assert_eq!(labels(&result), vec!["Asia", "Europe"]);
2360 let mut roots = result.row_roots.clone();
2361 sort_axis_by_key(&mut result.row_nodes, &mut roots, true);
2362 result.row_roots = roots;
2363 assert_eq!(labels(&result), vec!["Europe", "Asia"]);
2364 let asia = result.row_roots[1];
2365 let child_labels: Vec<&str> = result.row_nodes[asia]
2366 .children
2367 .iter()
2368 .map(|&c| result.row_nodes[c].label.as_str())
2369 .collect();
2370 assert_eq!(child_labels, vec!["Widget", "Gadget"]);
2371 }
2372
2373 #[test]
2374 fn sort_axis_by_values_orders_missing_first_ascending() {
2375 let cfg = two_level_config();
2376 let mut result = fixture_result(&cfg);
2377 let y2024 = result
2379 .col_roots
2380 .iter()
2381 .copied()
2382 .find(|&c| result.col_nodes[c].sort_key == Integer(2024))
2383 .unwrap();
2384 let keys: Vec<CellValue> = (0..result.row_nodes.len())
2385 .map(|id| {
2386 result
2387 .values
2388 .get(&(id, y2024))
2389 .cloned()
2390 .unwrap_or(CellValue::None)
2391 })
2392 .collect();
2393 let mut roots = result.row_roots.clone();
2394 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, false);
2395 let labels: Vec<&str> = roots
2396 .iter()
2397 .map(|&r| result.row_nodes[r].label.as_str())
2398 .collect();
2399 assert_eq!(labels, vec!["Asia", "Europe"]);
2400 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, true);
2401 let labels: Vec<&str> = roots
2402 .iter()
2403 .map(|&r| result.row_nodes[r].label.as_str())
2404 .collect();
2405 assert_eq!(labels, vec!["Europe", "Asia"]);
2406 }
2407
2408 #[test]
2409 fn node_path_walks_to_root() {
2410 let cfg = two_level_config();
2411 let result = fixture_result(&cfg);
2412 let europe = result
2413 .row_nodes
2414 .iter()
2415 .position(|n| n.label == "Europe")
2416 .unwrap();
2417 let widget = result.row_nodes[europe]
2418 .children
2419 .iter()
2420 .copied()
2421 .find(|&c| result.row_nodes[c].label == "Widget")
2422 .unwrap();
2423 assert_eq!(
2424 node_path(&result.row_nodes, widget),
2425 vec!["Europe".to_owned(), "Widget".to_owned()]
2426 );
2427 }
2428
2429 #[test]
2430 fn all_group_paths_lists_only_non_leaves() {
2431 let cfg = two_level_config();
2432 let result = fixture_result(&cfg);
2433 let paths = all_group_paths(&result.row_nodes);
2434 assert_eq!(paths.len(), 2); assert!(paths.contains(&vec!["Asia".to_owned()]));
2436 }
2437
2438 #[test]
2439 fn csv_line_quotes_fields_with_commas_and_quotes() {
2440 let mut out = String::new();
2441 push_csv_line(
2442 &mut out,
2443 &["a,b".to_owned(), "plain".to_owned(), "q\"q".to_owned()],
2444 );
2445 assert_eq!(out, "\"a,b\",plain,\"q\"\"q\"\n");
2446 }
2447
2448 fn fixture_columns() -> Vec<Column> {
2449 vec![
2450 Column::new("region", ColumnKind::Text, 100.0),
2451 Column::new("product", ColumnKind::Text, 100.0),
2452 Column::new("year", ColumnKind::Integer, 80.0),
2453 Column::new("amount", ColumnKind::Decimal, 100.0),
2454 ]
2455 }
2456
2457 fn fixture_rows() -> Vec<Vec<CellValue>> {
2458 let r = |region: &str, product: &str, year: i64, amount: f64| {
2459 vec![
2460 Text(region.into()),
2461 Text(product.into()),
2462 Integer(year),
2463 Decimal(amount),
2464 ]
2465 };
2466 vec![
2467 r("Europe", "Widget", 2023, 10.0),
2468 r("Europe", "Widget", 2024, 20.0),
2469 r("Europe", "Gadget", 2023, 5.0),
2470 r("Asia", "Widget", 2023, 7.0),
2471 r("Asia", "Gadget", 2024, 3.0),
2472 ]
2473 }
2474
2475 #[test]
2476 fn path_components_walk_root_to_leaf_with_field_metadata() {
2477 let cfg = two_level_config();
2478 let result = fixture_result(&cfg);
2479 let columns = fixture_columns();
2480 let europe = result
2481 .row_nodes
2482 .iter()
2483 .position(|n| n.label == "Europe")
2484 .unwrap();
2485 let widget = result.row_nodes[europe]
2486 .children
2487 .iter()
2488 .copied()
2489 .find(|&c| result.row_nodes[c].label == "Widget")
2490 .unwrap();
2491 let path = path_components(
2492 &result.row_nodes,
2493 &cfg.row_fields,
2494 &columns,
2495 "(blank)",
2496 widget,
2497 );
2498 assert_eq!(path.len(), 2);
2499 assert_eq!(path[0].field_name, "region");
2500 assert_eq!(path[0].label, "Europe");
2501 assert_eq!(path[0].field_index, 0);
2502 assert_eq!(path[1].field_name, "product");
2503 assert_eq!(path[1].label, "Widget");
2504 assert!(!path[1].is_blank);
2505 assert!(path_components(
2507 &result.row_nodes,
2508 &cfg.row_fields,
2509 &columns,
2510 "(blank)",
2511 TOTAL_KEY
2512 )
2513 .is_empty());
2514 }
2515
2516 #[test]
2517 fn rows_matching_path_selects_only_driving_rows() {
2518 let rows = fixture_rows();
2519 let columns = fixture_columns();
2520 let formats = GridConfig::default().resolve_all(&columns);
2521 let base: Vec<usize> = (0..rows.len()).collect();
2522 let constraints = vec![(0, "Europe".to_owned()), (1, "Widget".to_owned())];
2524 assert_eq!(
2525 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2526 vec![0, 1]
2527 );
2528 assert_eq!(
2530 rows_matching_path(&rows, &formats, "(blank)", &base, &[]),
2531 base
2532 );
2533 assert_eq!(
2535 rows_matching_path(&rows, &formats, "(blank)", &[1, 2, 3], &constraints),
2536 vec![1]
2537 );
2538 }
2539
2540 #[test]
2541 fn rows_matching_path_buckets_nulls_under_blank_label() {
2542 let mut rows = fixture_rows();
2543 rows.push(vec![
2544 CellValue::None,
2545 Text("Widget".into()),
2546 Integer(2023),
2547 Decimal(2.0),
2548 ]);
2549 let columns = fixture_columns();
2550 let formats = GridConfig::default().resolve_all(&columns);
2551 let base: Vec<usize> = (0..rows.len()).collect();
2552 let constraints = vec![(0, "(blank)".to_owned())];
2553 assert_eq!(
2554 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2555 vec![5]
2556 );
2557 }
2558
2559 #[test]
2560 fn drill_filter_set_maps_blank_to_empty_formatted_value() {
2561 let set = drill_filter_set("(blank)", "(blank)");
2562 assert!(set.contains(""));
2563 assert!(set.contains("(blank)"));
2564 let set = drill_filter_set("Europe", "(blank)");
2565 assert_eq!(set, HashSet::from(["Europe".to_owned()]));
2566 }
2567}