1use crate::config::{KeyBindings, NumberFormat, ResolvedColumnFormat};
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;
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::sync::Arc;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum VisibleRowKind {
32 Leaf,
34 GroupHeader {
37 expanded: bool,
39 },
40 GrandTotal,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct VisibleRow {
47 pub key: usize,
50 pub depth: usize,
52 pub kind: VisibleRowKind,
54 pub zebra: bool,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum VisibleColKind {
62 Leaf,
64 Collapsed,
66 Subtotal,
69 GrandTotal,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct VisibleCol {
76 pub key: usize,
79 pub depth: usize,
81 pub kind: VisibleColKind,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum PivotSortKey {
88 RowLabel,
90 RowsByColumn(usize),
94 ColLabel,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum PivotHitResult {
102 None,
104 Corner,
106 ColHeader {
108 level: usize,
110 col: usize,
112 },
113 RowHeader {
115 row: usize,
117 },
118 RowBorder {
120 row: usize,
122 },
123 ColBorder {
125 col: usize,
127 },
128 RowChevron {
130 row: usize,
132 },
133 ColChevron {
135 col: usize,
137 depth: usize,
139 },
140 Cell {
142 row: usize,
144 col: usize,
146 },
147 VScrollbar,
149 HScrollbar,
151}
152
153#[derive(Clone, Debug)]
155pub struct PivotFilterPopover {
156 pub field: usize,
158 pub rows: Vec<FilterValueRow>,
160 pub anchor: Point<Pixels>,
162}
163
164impl PivotFilterPopover {
165 #[must_use]
167 pub fn all_checked(&self) -> bool {
168 !self.rows.is_empty() && self.rows.iter().all(|r| r.checked)
169 }
170}
171
172#[derive(Clone, Debug)]
175pub(crate) struct PivotMenu {
176 pub(crate) anchor: Point<Pixels>,
178 pub(crate) items: Vec<PivotMenuItem>,
180 pub(crate) hovered: Option<usize>,
182 pub(crate) request: PivotContextMenuRequest,
184 pub(crate) drill: Vec<(usize, HashSet<String>)>,
187}
188
189pub(crate) const ROW_INDENT: f32 = 16.0;
191pub(crate) const CHEVRON_SIZE: f32 = 14.0;
193const RESIZE_HIT_SLOP: f32 = 3.0;
194pub const DEFAULT_PIVOT_ROW_HEIGHT: f32 = 24.0;
196pub const DEFAULT_PIVOT_COLUMN_WIDTH: f32 = 140.0;
198pub const MIN_PIVOT_ROW_HEIGHT: f32 = 18.0;
200pub const MIN_PIVOT_COLUMN_WIDTH: f32 = 40.0;
202
203#[derive(Clone, Copy, Debug)]
204enum PivotResizeDrag {
205 Row {
206 boundary: usize,
207 start_y: f32,
208 start_height: f32,
209 },
210 Column {
211 boundary: usize,
212 start_x: f32,
213 start_width: f32,
214 },
215}
216
217pub struct PivotState {
219 pub config: PivotConfig,
223 pub result: Arc<PivotResult>,
225 pub(crate) source_rows: Arc<Vec<Vec<CellValue>>>,
227 pub(crate) source_columns: Vec<Column>,
229 pub(crate) resolved_formats: Vec<ResolvedColumnFormat>,
231 pub(crate) value_fmt: ResolvedColumnFormat,
233
234 pub(crate) visible_rows: Arc<Vec<VisibleRow>>,
237 pub(crate) visible_cols: Arc<Vec<VisibleCol>>,
239 pub(crate) collapsed_row_paths: HashSet<Vec<String>>,
242 pub(crate) collapsed_col_paths: HashSet<Vec<String>>,
244 pub sort: Option<(PivotSortKey, SortDirection)>,
246 pub(crate) filter_values: HashMap<usize, HashSet<String>>,
249 pub(crate) filter_popover: Option<PivotFilterPopover>,
251 pub agg_menu_open: bool,
253 pub(crate) context_menu_provider: Option<PivotContextMenuProviderHandle>,
255 pub(crate) menu: Option<PivotMenu>,
257 pub(crate) pending_drill_down: Option<Vec<(usize, HashSet<String>)>>,
262
263 pub selection: Option<(usize, usize, usize, usize)>,
266 pub(crate) select_anchor: Option<(usize, usize)>,
267 pub(crate) is_selecting: bool,
268 pub hover_hit: Option<PivotHitResult>,
270 pub(crate) scrollbar_drag: Option<ScrollbarAxis>,
271 resize_drag: Option<PivotResizeDrag>,
272
273 pub theme: GridTheme,
275 pub(crate) key_bindings: KeyBindings,
276 pub scroll_handle: ScrollHandle,
278 pub focus_handle: FocusHandle,
280 pub bounds: Bounds<Pixels>,
282 pub(crate) window_viewport: Size<Pixels>,
283 pub row_height: f32,
285 pub header_row_height: f32,
287 pub row_header_width: f32,
289 pub value_col_width: f32,
291 pub font_size: f32,
293 pub char_width: f32,
295}
296
297impl PivotState {
298 #[must_use]
301 pub fn new(
302 source_columns: Vec<Column>,
303 source_rows: Arc<Vec<Vec<CellValue>>>,
304 resolved_formats: Vec<ResolvedColumnFormat>,
305 config: PivotConfig,
306 key_bindings: KeyBindings,
307 focus_handle: FocusHandle,
308 ) -> Self {
309 let mut state = Self {
310 config,
311 result: Arc::new(PivotResult::default()),
312 source_rows,
313 source_columns,
314 resolved_formats,
315 value_fmt: default_value_format(),
316 visible_rows: Arc::new(Vec::new()),
317 visible_cols: Arc::new(Vec::new()),
318 collapsed_row_paths: HashSet::new(),
319 collapsed_col_paths: HashSet::new(),
320 sort: None,
321 filter_values: HashMap::new(),
322 filter_popover: None,
323 agg_menu_open: false,
324 context_menu_provider: None,
325 menu: None,
326 pending_drill_down: None,
327 selection: None,
328 select_anchor: None,
329 is_selecting: false,
330 hover_hit: None,
331 scrollbar_drag: None,
332 theme: GridTheme::default(),
333 key_bindings,
334 scroll_handle: ScrollHandle::new(),
335 focus_handle,
336 bounds: Bounds::default(),
337 window_viewport: Size::default(),
338 row_height: DEFAULT_PIVOT_ROW_HEIGHT,
339 header_row_height: 26.0,
340 row_header_width: 220.0,
341 value_col_width: DEFAULT_PIVOT_COLUMN_WIDTH,
342 font_size: 13.0,
343 char_width: 7.9,
344 resize_drag: None,
345 };
346 state.recompute();
347 state
348 }
349
350 pub fn set_source(&mut self, columns: Vec<Column>, rows: Arc<Vec<Vec<CellValue>>>) {
353 self.source_columns = columns;
354 self.source_rows = rows;
355 self.recompute();
356 }
357
358 #[must_use]
360 pub fn source_differs(&self, rows: &Arc<Vec<Vec<CellValue>>>) -> bool {
361 !Arc::ptr_eq(&self.source_rows, rows)
362 }
363
364 #[must_use]
366 pub fn source_columns(&self) -> &[Column] {
367 &self.source_columns
368 }
369
370 pub fn recompute(&mut self) {
373 self.config.clamp_to_columns(self.source_columns.len());
374 let filter_fields = self.config.filter_fields.clone();
375 self.filter_values
376 .retain(|field, _| filter_fields.contains(field));
377
378 if self.config.is_ready() {
379 let included = self.filtered_source_rows();
380 self.result = Arc::new(compute_pivot(
381 &self.source_columns,
382 &self.source_rows,
383 &included,
384 &self.config,
385 &self.resolved_formats,
386 ));
387 } else {
388 self.result = Arc::new(PivotResult::default());
389 }
390 self.value_fmt = self.build_value_format();
391 self.resort();
392 }
393
394 fn filtered_source_rows(&self) -> Vec<usize> {
396 let active: Vec<(usize, &HashSet<String>)> = self
397 .config
398 .filter_fields
399 .iter()
400 .filter_map(|&f| self.filter_values.get(&f).map(|set| (f, set)))
401 .collect();
402 if active.is_empty() {
403 return (0..self.source_rows.len()).collect();
404 }
405 (0..self.source_rows.len())
406 .filter(|&r| {
407 active.iter().all(|(field, allowed)| {
408 let cell = self.source_rows[r].get(*field).unwrap_or(&CellValue::None);
409 let label = format_cell(cell, &self.resolved_formats[*field]).0;
410 allowed.contains(&label)
411 })
412 })
413 .collect()
414 }
415
416 fn build_value_format(&self) -> ResolvedColumnFormat {
421 let mut fmt = self
422 .config
423 .value_field
424 .and_then(|f| self.resolved_formats.get(f).cloned())
425 .unwrap_or_else(default_value_format);
426 match self.config.aggregation {
427 AggregationFn::Count => {
428 fmt.kind = ColumnKind::Integer;
429 fmt.number = NumberFormat {
430 decimals: 0,
431 ..fmt.number
432 };
433 }
434 AggregationFn::Avg => fmt.kind = ColumnKind::Decimal,
435 AggregationFn::Sum | AggregationFn::Min | AggregationFn::Max => {}
436 }
437 if let Some(over) = self.config.value_format {
438 fmt.number = over;
439 }
440 fmt
441 }
442
443 #[must_use]
445 pub fn value_format(&self) -> &ResolvedColumnFormat {
446 &self.value_fmt
447 }
448
449 pub(crate) fn rebuild_visible(&mut self) {
456 self.visible_rows = Arc::new(flatten_rows(
457 &self.result,
458 &self.collapsed_row_paths,
459 &self.config,
460 ));
461 self.visible_cols = Arc::new(flatten_cols(
462 &self.result,
463 &self.collapsed_col_paths,
464 &self.config,
465 ));
466 self.clamp_selection();
467 self.clamp_scroll_to_bounds();
468 }
469
470 #[must_use]
472 pub fn visible_rows(&self) -> &[VisibleRow] {
473 &self.visible_rows
474 }
475
476 #[must_use]
478 pub fn visible_cols(&self) -> &[VisibleCol] {
479 &self.visible_cols
480 }
481
482 pub fn toggle_row_group(&mut self, node: usize) {
485 if node >= self.result.row_nodes.len() {
486 return;
487 }
488 let path = node_path(&self.result.row_nodes, node);
489 if !self.collapsed_row_paths.remove(&path) {
490 self.collapsed_row_paths.insert(path);
491 }
492 self.rebuild_visible();
493 }
494
495 pub fn toggle_col_group(&mut self, node: usize) {
497 if node >= self.result.col_nodes.len() {
498 return;
499 }
500 let path = node_path(&self.result.col_nodes, node);
501 if !self.collapsed_col_paths.remove(&path) {
502 self.collapsed_col_paths.insert(path);
503 }
504 self.rebuild_visible();
505 }
506
507 pub fn collapse_all_rows(&mut self) {
509 self.collapsed_row_paths = all_group_paths(&self.result.row_nodes);
510 self.rebuild_visible();
511 }
512
513 pub fn expand_all_rows(&mut self) {
515 self.collapsed_row_paths.clear();
516 self.rebuild_visible();
517 }
518
519 pub fn collapse_all_cols(&mut self) {
521 self.collapsed_col_paths = all_group_paths(&self.result.col_nodes);
522 self.rebuild_visible();
523 }
524
525 pub fn expand_all_cols(&mut self) {
527 self.collapsed_col_paths.clear();
528 self.rebuild_visible();
529 }
530
531 pub fn cycle_sort_by_column(&mut self, col_key: usize) {
538 self.sort = match self.sort {
539 Some((PivotSortKey::RowsByColumn(k), SortDirection::Ascending)) if k == col_key => {
540 Some((
541 PivotSortKey::RowsByColumn(col_key),
542 SortDirection::Descending,
543 ))
544 }
545 Some((PivotSortKey::RowsByColumn(k), SortDirection::Descending)) if k == col_key => {
546 None
547 }
548 _ => Some((
549 PivotSortKey::RowsByColumn(col_key),
550 SortDirection::Ascending,
551 )),
552 };
553 self.resort();
554 }
555
556 pub fn cycle_label_sort(&mut self) {
559 self.sort = match self.sort {
560 Some((PivotSortKey::RowLabel, SortDirection::Ascending)) => {
561 Some((PivotSortKey::RowLabel, SortDirection::Descending))
562 }
563 Some((PivotSortKey::RowLabel, SortDirection::Descending)) => None,
564 _ => Some((PivotSortKey::RowLabel, SortDirection::Ascending)),
565 };
566 self.resort();
567 }
568
569 pub fn cycle_col_label_sort(&mut self) {
571 self.sort = match self.sort {
572 Some((PivotSortKey::ColLabel, SortDirection::Ascending)) => {
573 Some((PivotSortKey::ColLabel, SortDirection::Descending))
574 }
575 Some((PivotSortKey::ColLabel, SortDirection::Descending)) => None,
576 _ => Some((PivotSortKey::ColLabel, SortDirection::Ascending)),
577 };
578 self.resort();
579 }
580
581 pub(crate) fn resort(&mut self) {
585 let result = Arc::make_mut(&mut self.result);
586 sort_axis_by_key(&mut result.row_nodes, &mut result.row_roots, false);
589 sort_axis_by_key(&mut result.col_nodes, &mut result.col_roots, false);
590 match self.sort {
591 None => {}
592 Some((PivotSortKey::RowLabel, dir)) => {
593 sort_axis_by_key(
594 &mut result.row_nodes,
595 &mut result.row_roots,
596 dir == SortDirection::Descending,
597 );
598 }
599 Some((PivotSortKey::ColLabel, dir)) => {
600 sort_axis_by_key(
601 &mut result.col_nodes,
602 &mut result.col_roots,
603 dir == SortDirection::Descending,
604 );
605 }
606 Some((PivotSortKey::RowsByColumn(col_key), dir)) => {
607 let keys: Vec<CellValue> = (0..result.row_nodes.len())
608 .map(|id| {
609 result
610 .values
611 .get(&(id, col_key))
612 .cloned()
613 .unwrap_or(CellValue::None)
614 })
615 .collect();
616 sort_axis_by(
617 &mut result.row_nodes,
618 &mut result.row_roots,
619 &keys,
620 dir == SortDirection::Descending,
621 );
622 }
623 }
624 self.rebuild_visible();
625 }
626
627 pub fn open_filter_popover(&mut self, field: usize, anchor: Point<Pixels>) {
634 if field >= self.source_columns.len() {
635 return;
636 }
637 let fmt = &self.resolved_formats[field];
638 let allowed = self.filter_values.get(&field);
639 let mut seen: HashSet<String> = HashSet::new();
640 let mut pairs: Vec<(String, CellValue)> = Vec::new();
641 for row in self.source_rows.iter() {
642 let cell = row.get(field).unwrap_or(&CellValue::None);
643 let label = format_cell(cell, fmt).0;
644 if seen.insert(label.clone()) {
645 pairs.push((label, cell.clone()));
646 }
647 }
648 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
649 let rows = pairs
650 .into_iter()
651 .map(|(label, _)| {
652 let checked = allowed.is_none_or(|set| set.contains(&label));
653 FilterValueRow { label, checked }
654 })
655 .collect();
656 self.filter_popover = Some(PivotFilterPopover {
657 field,
658 rows,
659 anchor,
660 });
661 }
662
663 #[must_use]
665 pub fn filter_popover(&self) -> Option<&PivotFilterPopover> {
666 self.filter_popover.as_ref()
667 }
668
669 pub fn toggle_filter_popover_value(&mut self, index: usize) {
671 if let Some(p) = &mut self.filter_popover {
672 if let Some(row) = p.rows.get_mut(index) {
673 row.checked = !row.checked;
674 }
675 }
676 self.apply_filter_popover();
677 }
678
679 pub fn toggle_filter_popover_select_all(&mut self) {
681 if let Some(p) = &mut self.filter_popover {
682 let target = !p.all_checked();
683 for row in &mut p.rows {
684 row.checked = target;
685 }
686 }
687 self.apply_filter_popover();
688 }
689
690 pub fn apply_filter_popover(&mut self) {
693 let Some(p) = &self.filter_popover else {
694 return;
695 };
696 let field = p.field;
697 if p.all_checked() {
698 self.filter_values.remove(&field);
699 } else {
700 self.filter_values.insert(
701 field,
702 p.rows
703 .iter()
704 .filter(|r| r.checked)
705 .map(|r| r.label.clone())
706 .collect(),
707 );
708 }
709 self.recompute();
710 }
711
712 pub fn close_filter_popover(&mut self) {
714 self.filter_popover = None;
715 }
716
717 pub fn clear_filter(&mut self, field: usize) {
719 if self.filter_values.remove(&field).is_some() {
720 self.recompute();
721 }
722 if let Some(p) = &mut self.filter_popover {
723 if p.field == field {
724 for row in &mut p.rows {
725 row.checked = true;
726 }
727 }
728 }
729 }
730
731 #[must_use]
734 pub fn filter_active(&self, field: usize) -> bool {
735 self.filter_values.contains_key(&field)
736 }
737
738 pub fn set_context_menu_provider(
746 &mut self,
747 provider: impl crate::pivot::context_menu::PivotContextMenuProvider + 'static,
748 ) {
749 self.context_menu_provider = Some(PivotContextMenuProviderHandle::new(provider));
750 }
751
752 #[must_use]
754 pub fn row_path_components(&self, row_key: usize) -> Vec<PivotPathComponent> {
755 path_components(
756 &self.result.row_nodes,
757 &self.config.row_fields,
758 &self.source_columns,
759 &self.config.blank_label,
760 row_key,
761 )
762 }
763
764 #[must_use]
766 pub fn col_path_components(&self, col_key: usize) -> Vec<PivotPathComponent> {
767 path_components(
768 &self.result.col_nodes,
769 &self.config.column_fields,
770 &self.source_columns,
771 &self.config.blank_label,
772 col_key,
773 )
774 }
775
776 #[must_use]
781 pub fn source_rows_for(&self, row_key: usize, col_key: usize) -> Vec<usize> {
782 let constraints: Vec<(usize, String)> = self
783 .row_path_components(row_key)
784 .into_iter()
785 .chain(self.col_path_components(col_key))
786 .map(|c| (c.field_index, c.label))
787 .collect();
788 rows_matching_path(
789 &self.source_rows,
790 &self.resolved_formats,
791 &self.config.blank_label,
792 &self.filtered_source_rows(),
793 &constraints,
794 )
795 }
796
797 #[must_use]
803 pub fn drill_down_filters(
804 &self,
805 row_key: usize,
806 col_key: usize,
807 ) -> Vec<(usize, HashSet<String>)> {
808 let mut out: Vec<(usize, HashSet<String>)> = self
809 .config
810 .filter_fields
811 .iter()
812 .filter_map(|&f| self.filter_values.get(&f).map(|set| (f, set.clone())))
813 .collect();
814 for comp in self
815 .row_path_components(row_key)
816 .into_iter()
817 .chain(self.col_path_components(col_key))
818 {
819 let set = drill_filter_set(&comp.label, &self.config.blank_label);
820 if let Some(entry) = out.iter_mut().find(|(f, _)| *f == comp.field_index) {
821 entry.1 = set;
822 } else {
823 out.push((comp.field_index, set));
824 }
825 }
826 out
827 }
828
829 pub fn request_drill_down(&mut self, row: usize, col: usize) {
834 let (Some(vr), Some(vc)) = (
835 self.visible_rows.get(row).copied(),
836 self.visible_cols.get(col).copied(),
837 ) else {
838 return;
839 };
840 self.pending_drill_down = Some(self.drill_down_filters(vr.key, vc.key));
841 }
842
843 pub(crate) fn take_pending_drill_down(&mut self) -> Option<Vec<(usize, HashSet<String>)>> {
845 self.pending_drill_down.take()
846 }
847
848 fn cell_context(&self, vr: &VisibleRow, vc: &VisibleCol) -> PivotCellContext {
850 let value = self.result.value(vr.key, vc.key).clone();
851 let formatted_value = if matches!(value, CellValue::None) {
852 String::new()
853 } else {
854 format_cell(&value, &self.value_fmt).0
855 };
856 PivotCellContext {
857 row_path: self.row_path_components(vr.key),
858 col_path: self.col_path_components(vc.key),
859 value,
860 formatted_value,
861 is_row_grand_total: vr.kind == VisibleRowKind::GrandTotal,
862 is_col_grand_total: vc.kind == VisibleColKind::GrandTotal,
863 is_row_subtotal: matches!(vr.kind, VisibleRowKind::GroupHeader { .. }),
864 is_col_subtotal: matches!(
865 vc.kind,
866 VisibleColKind::Subtotal | VisibleColKind::Collapsed
867 ),
868 }
869 }
870
871 pub fn open_context_menu(&mut self, hit: PivotHitResult, anchor: Point<Pixels>) {
876 self.filter_popover = None;
877 let resolved: Option<(PivotMenuTarget, usize, usize)> = match hit {
878 PivotHitResult::Cell { row, col } => {
879 match (self.visible_rows.get(row), self.visible_cols.get(col)) {
880 (Some(vr), Some(vc)) => {
881 let ctx = self.cell_context(vr, vc);
882 Some((PivotMenuTarget::Cell(ctx), vr.key, vc.key))
883 }
884 _ => None,
885 }
886 }
887 PivotHitResult::RowHeader { row } | PivotHitResult::RowChevron { row } => {
888 self.visible_rows.get(row).map(|vr| {
889 (
890 PivotMenuTarget::RowHeader {
891 path: self.row_path_components(vr.key),
892 is_grand_total: vr.kind == VisibleRowKind::GrandTotal,
893 },
894 vr.key,
895 TOTAL_KEY,
896 )
897 })
898 }
899 PivotHitResult::ColHeader { level: 0, .. } | PivotHitResult::Corner => {
900 Some((PivotMenuTarget::Corner, TOTAL_KEY, TOTAL_KEY))
901 }
902 PivotHitResult::ColHeader { level, col } => {
903 let depth = level - 1;
904 let key = self
905 .col_ancestor_at(col, depth)
906 .or_else(|| self.visible_cols.get(col).map(|vc| vc.key));
907 key.map(|key| {
908 let is_grand = key == TOTAL_KEY
909 && self
910 .visible_cols
911 .get(col)
912 .is_some_and(|vc| vc.kind == VisibleColKind::GrandTotal);
913 (
914 PivotMenuTarget::ColHeader {
915 path: self.col_path_components(key),
916 is_grand_total: is_grand,
917 },
918 TOTAL_KEY,
919 key,
920 )
921 })
922 }
923 PivotHitResult::ColChevron { col, depth } => {
924 self.col_group_run_at(col, depth).map(|(node, _)| {
925 (
926 PivotMenuTarget::ColHeader {
927 path: self.col_path_components(node),
928 is_grand_total: false,
929 },
930 TOTAL_KEY,
931 node,
932 )
933 })
934 }
935 PivotHitResult::None
936 | PivotHitResult::RowBorder { .. }
937 | PivotHitResult::ColBorder { .. }
938 | PivotHitResult::VScrollbar
939 | PivotHitResult::HScrollbar => None,
940 };
941 let Some((target, row_key, col_key)) = resolved else {
942 self.menu = None;
943 return;
944 };
945
946 let request = PivotContextMenuRequest {
947 source_row_indices: self.source_rows_for(row_key, col_key),
948 target,
949 aggregation: self.config.aggregation,
950 value_field_index: self.config.value_field,
951 value_caption: self.result.value_caption.clone(),
952 config: self.config.clone(),
953 };
954 let items = match &self.context_menu_provider {
955 Some(provider) => provider.menu_items(&request),
956 None => PivotMenuItem::standard_items(&request.target),
957 };
958 if items.is_empty() {
959 self.menu = None;
960 return;
961 }
962 let drill = self.drill_down_filters(row_key, col_key);
963 self.menu = Some(PivotMenu {
964 anchor,
965 items,
966 hovered: None,
967 request,
968 drill,
969 });
970 }
971
972 pub(crate) fn execute_menu_action(&mut self, id: &str, menu: PivotMenu, cx: &mut App) {
975 match id {
976 PIVOT_ACTION_SHOW_SOURCE_ROWS => {
977 self.pending_drill_down = Some(menu.drill);
978 }
979 PIVOT_ACTION_COPY_VALUE => {
980 if let Some(cell) = menu.request.clicked_cell() {
981 cx.write_to_clipboard(gpui::ClipboardItem::new_string(
982 cell.formatted_value.clone(),
983 ));
984 }
985 }
986 PIVOT_ACTION_COPY_CSV => {
987 let csv = self.to_csv();
988 if !csv.is_empty() {
989 cx.write_to_clipboard(gpui::ClipboardItem::new_string(csv));
990 }
991 }
992 _ => {
993 if let Some(provider) = self.context_menu_provider.clone() {
994 provider.on_action(id, &menu.request, self, cx);
995 }
996 }
997 }
998 }
999
1000 #[must_use]
1006 pub fn row_label(&self, row: &VisibleRow) -> String {
1007 match row.kind {
1008 VisibleRowKind::GrandTotal => "Grand Total".into(),
1009 _ if row.key == TOTAL_KEY => "Total".into(),
1010 _ => self
1011 .result
1012 .row_nodes
1013 .get(row.key)
1014 .map(|n| n.label.clone())
1015 .unwrap_or_default(),
1016 }
1017 }
1018
1019 #[must_use]
1022 pub fn col_label(&self, col: &VisibleCol) -> String {
1023 match col.kind {
1024 VisibleColKind::GrandTotal => "Grand Total".into(),
1025 VisibleColKind::Subtotal | VisibleColKind::Collapsed => self
1026 .result
1027 .col_nodes
1028 .get(col.key)
1029 .map(|n| format!("{} Total", n.label))
1030 .unwrap_or_else(|| "Total".into()),
1031 _ if col.key == TOTAL_KEY => self.result.value_caption.clone(),
1032 _ => self
1033 .result
1034 .col_nodes
1035 .get(col.key)
1036 .map(|n| n.label.clone())
1037 .unwrap_or_default(),
1038 }
1039 }
1040
1041 #[must_use]
1043 pub fn cell_value(&self, row: &VisibleRow, col: &VisibleCol) -> &CellValue {
1044 self.result.value(row.key, col.key)
1045 }
1046
1047 #[must_use]
1053 pub fn row_height(&self) -> f32 {
1054 self.row_height
1055 }
1056
1057 pub fn set_row_height(&mut self, height: f32) {
1062 if height.is_finite() {
1063 self.row_height = height.max(MIN_PIVOT_ROW_HEIGHT);
1064 self.clamp_scroll_to_bounds();
1065 }
1066 }
1067
1068 #[must_use]
1070 pub fn column_width(&self) -> f32 {
1071 self.value_col_width
1072 }
1073
1074 pub fn set_column_width(&mut self, width: f32) {
1079 if width.is_finite() {
1080 self.value_col_width = width.max(MIN_PIVOT_COLUMN_WIDTH);
1081 self.clamp_scroll_to_bounds();
1082 }
1083 }
1084
1085 #[must_use]
1088 pub fn header_levels(&self) -> usize {
1089 1 + self.result.col_depth.max(1)
1090 }
1091
1092 #[must_use]
1094 pub fn header_height(&self) -> f32 {
1095 self.header_levels() as f32 * self.header_row_height
1096 }
1097
1098 #[must_use]
1100 pub fn content_size(&self) -> (f32, f32) {
1101 (
1102 self.visible_cols.len() as f32 * self.value_col_width,
1103 self.visible_rows.len() as f32 * self.row_height,
1104 )
1105 }
1106
1107 pub(crate) fn scrollbar_reserved(&self) -> (f32, f32) {
1108 let (cw, ch) = self.content_size();
1109 let vw = f32::from(self.bounds.size.width) - self.row_header_width;
1110 let vh = f32::from(self.bounds.size.height) - self.header_height();
1111 let reserved_w = if ch > vh { SCROLLBAR_SIZE } else { 0.0 };
1112 let reserved_h = if cw > vw { SCROLLBAR_SIZE } else { 0.0 };
1113 (reserved_w, reserved_h)
1114 }
1115
1116 pub(crate) fn max_scroll(&self) -> (f32, f32) {
1117 let (cw, ch) = self.content_size();
1118 let (rw, rh) = self.scrollbar_reserved();
1119 let vw = f32::from(self.bounds.size.width) - self.row_header_width - rw;
1120 let vh = f32::from(self.bounds.size.height) - self.header_height() - rh;
1121 ((cw - vw).max(0.0), (ch - vh).max(0.0))
1122 }
1123
1124 pub(crate) fn clamp_scroll_to_bounds(&mut self) {
1126 let (mx, my) = self.max_scroll();
1127 let s = self.scroll_handle.offset();
1128 let nx = f32::from(s.x).clamp(0.0, mx);
1129 let ny = f32::from(s.y).clamp(0.0, my);
1130 if nx != f32::from(s.x) || ny != f32::from(s.y) {
1131 self.scroll_handle.set_offset(Point {
1132 x: px(nx),
1133 y: px(ny),
1134 });
1135 }
1136 }
1137
1138 fn clamp_selection(&mut self) {
1139 let (nr, nc) = (self.visible_rows.len(), self.visible_cols.len());
1140 if let Some((r1, c1, r2, c2)) = self.selection {
1141 if nr == 0 || nc == 0 || r1 >= nr || c1 >= nc {
1142 self.selection = None;
1143 self.select_anchor = None;
1144 } else {
1145 self.selection = Some((r1, c1, r2.min(nr - 1), c2.min(nc - 1)));
1146 }
1147 }
1148 }
1149
1150 #[must_use]
1152 pub fn hit_test(&self, pos: Point<Pixels>) -> PivotHitResult {
1153 let x = f32::from(pos.x);
1154 let y = f32::from(pos.y);
1155 let bw = f32::from(self.bounds.size.width);
1156 let bh = f32::from(self.bounds.size.height);
1157 if x < 0.0 || y < 0.0 || x > bw || y > bh {
1158 return PivotHitResult::None;
1159 }
1160 let hdr_h = self.header_height();
1161 let (mx, my) = self.max_scroll();
1162 if my > 0.0 && x >= bw - SCROLLBAR_SIZE && y >= hdr_h {
1163 return PivotHitResult::VScrollbar;
1164 }
1165 if mx > 0.0 && y >= bh - SCROLLBAR_SIZE && x >= self.row_header_width {
1166 return PivotHitResult::HScrollbar;
1167 }
1168 let sx = f32::from(self.scroll_handle.offset().x);
1169 let sy = f32::from(self.scroll_handle.offset().y);
1170
1171 if y < hdr_h {
1172 if x < self.row_header_width {
1173 return PivotHitResult::Corner;
1174 }
1175 let level = ((y / self.header_row_height) as usize).min(self.header_levels() - 1);
1176 let cx = x - self.row_header_width + sx;
1177 if cx < 0.0 {
1178 return PivotHitResult::None;
1179 }
1180 let boundary = (cx / self.value_col_width).round() as usize;
1181 if boundary > 0
1182 && boundary <= self.visible_cols.len()
1183 && (cx - boundary as f32 * self.value_col_width).abs() <= RESIZE_HIT_SLOP
1184 {
1185 return PivotHitResult::ColBorder { col: boundary - 1 };
1186 }
1187 let col = (cx / self.value_col_width) as usize;
1188 if col >= self.visible_cols.len() {
1189 return PivotHitResult::None;
1190 }
1191 if level >= 1 {
1194 let depth = level - 1;
1195 if let Some((_, run_start)) = self.col_group_run_at(col, depth) {
1196 let run_x0 = run_start as f32 * self.value_col_width;
1197 let within = cx - run_x0;
1198 let is_group = self
1199 .col_ancestor_at(col, depth)
1200 .map(|n| !self.result.col_nodes[n].is_leaf())
1201 .unwrap_or(false);
1202 if is_group && (2.0..=2.0 + CHEVRON_SIZE).contains(&within) {
1203 return PivotHitResult::ColChevron {
1204 col: run_start,
1205 depth,
1206 };
1207 }
1208 }
1209 }
1210 return PivotHitResult::ColHeader { level, col };
1211 }
1212
1213 if x < self.row_header_width {
1214 let ry = y - hdr_h + sy;
1215 if ry < 0.0 {
1216 return PivotHitResult::None;
1217 }
1218 let boundary = (ry / self.row_height).round() as usize;
1219 if boundary > 0
1220 && boundary <= self.visible_rows.len()
1221 && (ry - boundary as f32 * self.row_height).abs() <= RESIZE_HIT_SLOP
1222 {
1223 return PivotHitResult::RowBorder { row: boundary - 1 };
1224 }
1225 let row = (ry / self.row_height) as usize;
1226 if row >= self.visible_rows.len() {
1227 return PivotHitResult::None;
1228 }
1229 let vr = self.visible_rows[row];
1230 if matches!(vr.kind, VisibleRowKind::GroupHeader { .. }) {
1231 let indent = vr.depth as f32 * ROW_INDENT + 4.0;
1232 if x >= indent && x <= indent + CHEVRON_SIZE {
1233 return PivotHitResult::RowChevron { row };
1234 }
1235 }
1236 return PivotHitResult::RowHeader { row };
1237 }
1238
1239 let cx = x - self.row_header_width + sx;
1240 let ry = y - hdr_h + sy;
1241 if cx < 0.0 || ry < 0.0 {
1242 return PivotHitResult::None;
1243 }
1244 let col = (cx / self.value_col_width) as usize;
1245 let row = (ry / self.row_height) as usize;
1246 if row < self.visible_rows.len() && col < self.visible_cols.len() {
1247 PivotHitResult::Cell { row, col }
1248 } else {
1249 PivotHitResult::None
1250 }
1251 }
1252
1253 pub(crate) fn col_group_run_at(&self, col: usize, depth: usize) -> Option<(usize, usize)> {
1257 let node = self.col_ancestor_at(col, depth)?;
1258 let mut start = col;
1259 while start > 0 && self.col_ancestor_at(start - 1, depth) == Some(node) {
1260 start -= 1;
1261 }
1262 Some((node, start))
1263 }
1264
1265 pub(crate) fn col_ancestor_at(&self, col: usize, depth: usize) -> Option<usize> {
1267 let vc = self.visible_cols.get(col)?;
1268 if vc.key == TOTAL_KEY {
1269 return None;
1270 }
1271 let mut id = vc.key;
1272 let mut d = self.result.col_nodes.get(id)?.depth;
1273 if depth > d {
1274 return None;
1275 }
1276 while d > depth {
1277 id = self.result.col_nodes[id].parent?;
1278 d -= 1;
1279 }
1280 Some(id)
1281 }
1282
1283 pub fn handle_mouse_down(&mut self, pos: Point<Pixels>, shift: bool) {
1289 let hit = self.hit_test(pos);
1290 match hit {
1291 PivotHitResult::VScrollbar => {
1292 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1293 self.scroll_to_vbar(f32::from(pos.y));
1294 }
1295 PivotHitResult::HScrollbar => {
1296 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1297 self.scroll_to_hbar(f32::from(pos.x));
1298 }
1299 PivotHitResult::RowBorder { row } => {
1300 self.resize_drag = Some(PivotResizeDrag::Row {
1301 boundary: row + 1,
1302 start_y: f32::from(pos.y),
1303 start_height: self.row_height,
1304 });
1305 }
1306 PivotHitResult::ColBorder { col } => {
1307 self.resize_drag = Some(PivotResizeDrag::Column {
1308 boundary: col + 1,
1309 start_x: f32::from(pos.x),
1310 start_width: self.value_col_width,
1311 });
1312 }
1313 PivotHitResult::RowChevron { row } => {
1314 if let Some(vr) = self.visible_rows.get(row).copied() {
1315 self.toggle_row_group(vr.key);
1316 }
1317 }
1318 PivotHitResult::ColChevron { col, depth } => {
1319 if let Some((node, _)) = self.col_group_run_at(col, depth) {
1320 self.toggle_col_group(node);
1321 } else if let Some(vc) = self.visible_cols.get(col) {
1322 if vc.key != TOTAL_KEY {
1323 self.toggle_col_group(vc.key);
1324 }
1325 }
1326 }
1327 PivotHitResult::Corner => self.cycle_label_sort(),
1328 PivotHitResult::ColHeader { level, col } => {
1329 if level == 0 {
1333 return;
1334 }
1335 if level == self.header_levels() - 1 {
1336 if let Some(vc) = self.visible_cols.get(col).copied() {
1337 self.cycle_sort_by_column(vc.key);
1338 }
1339 } else {
1340 let depth = level - 1;
1341 if let Some((node, _)) = self.col_group_run_at(col, depth) {
1342 if !self.result.col_nodes[node].is_leaf() {
1343 self.toggle_col_group(node);
1344 }
1345 }
1346 }
1347 }
1348 PivotHitResult::RowHeader { row } => {
1349 if self.visible_cols.is_empty() {
1350 return;
1351 }
1352 let last = self.visible_cols.len() - 1;
1353 self.selection = Some((row, 0, row, last));
1354 self.select_anchor = Some((row, 0));
1355 }
1356 PivotHitResult::Cell { row, col } => {
1357 if shift {
1358 let (ar, ac) = self.select_anchor.unwrap_or((row, col));
1359 self.selection = Some(norm_rect(ar, ac, row, col));
1360 } else {
1361 self.selection = Some((row, col, row, col));
1362 self.select_anchor = Some((row, col));
1363 self.is_selecting = true;
1364 }
1365 }
1366 PivotHitResult::None => {
1367 self.selection = None;
1368 self.select_anchor = None;
1369 }
1370 }
1371 }
1372
1373 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, left_down: bool) {
1375 if let Some(drag) = self.resize_drag {
1376 if !left_down {
1377 self.resize_drag = None;
1378 } else {
1379 match drag {
1380 PivotResizeDrag::Row {
1381 boundary,
1382 start_y,
1383 start_height,
1384 } => {
1385 let delta = (f32::from(pos.y) - start_y) / boundary as f32;
1386 self.set_row_height(start_height + delta);
1387 self.hover_hit = Some(PivotHitResult::RowBorder { row: boundary - 1 });
1388 }
1389 PivotResizeDrag::Column {
1390 boundary,
1391 start_x,
1392 start_width,
1393 } => {
1394 let delta = (f32::from(pos.x) - start_x) / boundary as f32;
1395 self.set_column_width(start_width + delta);
1396 self.hover_hit = Some(PivotHitResult::ColBorder { col: boundary - 1 });
1397 }
1398 }
1399 return;
1400 }
1401 }
1402 if let Some(axis) = self.scrollbar_drag {
1403 if left_down {
1404 match axis {
1405 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
1406 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
1407 }
1408 return;
1409 }
1410 self.scrollbar_drag = None;
1411 }
1412 let hit = self.hit_test(pos);
1413 self.hover_hit = Some(hit);
1414 if self.is_selecting && left_down {
1415 if let PivotHitResult::Cell { row, col } = hit {
1416 if let Some((ar, ac)) = self.select_anchor {
1417 self.selection = Some(norm_rect(ar, ac, row, col));
1418 }
1419 }
1420 } else if self.is_selecting {
1421 self.is_selecting = false;
1422 }
1423 }
1424
1425 pub fn handle_mouse_up(&mut self) {
1427 self.is_selecting = false;
1428 self.scrollbar_drag = None;
1429 self.resize_drag = None;
1430 }
1431
1432 pub fn apply_scroll_delta(&mut self, dx: f32, dy: f32) {
1434 let (mx, my) = self.max_scroll();
1435 let s = self.scroll_handle.offset();
1436 let nx = (f32::from(s.x) - dx).clamp(0.0, mx);
1437 let ny = (f32::from(s.y) - dy).clamp(0.0, my);
1438 self.scroll_handle.set_offset(Point {
1439 x: px(nx),
1440 y: px(ny),
1441 });
1442 }
1443
1444 pub(crate) fn scroll_to_vbar(&mut self, mouse_y: f32) {
1445 let (_, my) = self.max_scroll();
1446 let hdr = self.header_height();
1447 let (_, rh) = self.scrollbar_reserved();
1448 let track_h = f32::from(self.bounds.size.height) - hdr - rh;
1449 let (_, ch) = self.content_size();
1450 if track_h <= 0.0 || ch <= 0.0 {
1451 return;
1452 }
1453 let thumb_h = ((track_h * (track_h / ch)).max(20.0)).min(track_h);
1454 let range = (track_h - thumb_h).max(0.0);
1455 let rel = (mouse_y - hdr - thumb_h * 0.5).clamp(0.0, range);
1456 let frac = if range > 0.0 { rel / range } else { 0.0 };
1457 let x = self.scroll_handle.offset().x;
1458 self.scroll_handle.set_offset(Point {
1459 x,
1460 y: px(frac * my),
1461 });
1462 }
1463
1464 pub(crate) fn scroll_to_hbar(&mut self, mouse_x: f32) {
1465 let (mx, _) = self.max_scroll();
1466 let (rw, _) = self.scrollbar_reserved();
1467 let track_x = self.row_header_width;
1468 let track_w = f32::from(self.bounds.size.width) - self.row_header_width - rw;
1469 let (cw, _) = self.content_size();
1470 if track_w <= 0.0 || cw <= 0.0 {
1471 return;
1472 }
1473 let thumb_w = ((track_w * (track_w / cw)).max(20.0)).min(track_w);
1474 let range = (track_w - thumb_w).max(0.0);
1475 let rel = (mouse_x - track_x - thumb_w * 0.5).clamp(0.0, range);
1476 let frac = if range > 0.0 { rel / range } else { 0.0 };
1477 let y = self.scroll_handle.offset().y;
1478 self.scroll_handle.set_offset(Point {
1479 x: px(frac * mx),
1480 y,
1481 });
1482 }
1483
1484 pub fn handle_key(&mut self, ks: &gpui::Keystroke, cx: &mut App) {
1486 if self.key_bindings.copy.matches(ks) {
1487 self.copy_selection(false, cx);
1488 return;
1489 }
1490 if self.key_bindings.copy_with_headers.matches(ks) {
1491 self.copy_selection(true, cx);
1492 return;
1493 }
1494 match ks.key.as_str() {
1495 "escape" => {
1496 self.selection = None;
1497 self.select_anchor = None;
1498 self.filter_popover = None;
1499 self.menu = None;
1500 }
1501 "up" => self.move_selection(0, -1),
1502 "down" => self.move_selection(0, 1),
1503 "left" => self.move_selection(-1, 0),
1504 "right" => self.move_selection(1, 0),
1505 _ => {}
1506 }
1507 }
1508
1509 fn move_selection(&mut self, dx: i32, dy: i32) {
1510 let nr = self.visible_rows.len() as i32;
1511 let nc = self.visible_cols.len() as i32;
1512 if nr == 0 || nc == 0 {
1513 return;
1514 }
1515 let (r, c) = match self.selection {
1516 Some((r1, c1, ..)) => (r1 as i32, c1 as i32),
1517 None => (0, 0),
1518 };
1519 let r = (r + dy).clamp(0, nr - 1) as usize;
1520 let c = (c + dx).clamp(0, nc - 1) as usize;
1521 self.selection = Some((r, c, r, c));
1522 self.select_anchor = Some((r, c));
1523 }
1524
1525 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
1532 let text = self.selection_text(with_headers, '\t');
1533 if !text.is_empty() {
1534 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1535 }
1536 }
1537
1538 #[must_use]
1541 pub fn selection_text(&self, with_headers: bool, sep: char) -> String {
1542 use std::fmt::Write as _;
1543 if self.visible_rows.is_empty() || self.visible_cols.is_empty() {
1544 return String::new();
1545 }
1546 let (r1, c1, r2, c2) = self.selection.unwrap_or((
1547 0,
1548 0,
1549 self.visible_rows.len() - 1,
1550 self.visible_cols.len() - 1,
1551 ));
1552 let mut out = String::new();
1553 if with_headers {
1554 for c in c1..=c2 {
1555 out.push(sep);
1556 out.push_str(&self.col_label(&self.visible_cols[c]));
1557 }
1558 out.push('\n');
1559 }
1560 for r in r1..=r2 {
1561 let vr = &self.visible_rows[r];
1562 if with_headers {
1563 let _ = write!(out, "{}{}", " ".repeat(vr.depth), self.row_label(vr));
1564 out.push(sep);
1565 }
1566 for c in c1..=c2 {
1567 if c > c1 {
1568 out.push(sep);
1569 }
1570 let cell = self.cell_value(vr, &self.visible_cols[c]);
1571 if !matches!(cell, CellValue::None) {
1572 out.push_str(&format_cell(cell, &self.value_fmt).0);
1573 }
1574 }
1575 out.push('\n');
1576 }
1577 out
1578 }
1579
1580 #[must_use]
1584 pub fn to_csv(&self) -> String {
1585 let res = &self.result;
1586 let mut out = String::new();
1587 let col_leaves = res.col_leaves();
1588 let want_grand_col = self.config.show_column_grand_total && !col_leaves.is_empty();
1589
1590 let mut header: Vec<String> = if res.row_depth == 0 {
1591 vec![String::new()]
1592 } else {
1593 res.row_field_names.clone()
1594 };
1595 if col_leaves.is_empty() {
1596 header.push(res.value_caption.clone());
1597 } else {
1598 for &leaf in &col_leaves {
1599 header.push(node_path(&res.col_nodes, leaf).join(" / "));
1600 }
1601 }
1602 if want_grand_col {
1603 header.push("Grand Total".into());
1604 }
1605 push_csv_line(&mut out, &header);
1606
1607 let value_cols: Vec<usize> = if col_leaves.is_empty() {
1608 vec![TOTAL_KEY]
1609 } else {
1610 col_leaves
1611 };
1612 let fmt_value = |cell: &CellValue| {
1613 if matches!(cell, CellValue::None) {
1614 String::new()
1615 } else {
1616 format_cell(cell, &self.value_fmt).0
1617 }
1618 };
1619 let emit_line = |out: &mut String, labels: Vec<String>, row_key: usize| {
1620 let mut fields = labels;
1621 for &ck in &value_cols {
1622 fields.push(fmt_value(res.value(row_key, ck)));
1623 }
1624 if want_grand_col {
1625 fields.push(fmt_value(res.value(row_key, TOTAL_KEY)));
1626 }
1627 push_csv_line(out, &fields);
1628 };
1629
1630 let row_leaves = res.row_leaves();
1631 if row_leaves.is_empty() && res.row_depth == 0 && !res.values.is_empty() {
1632 emit_line(&mut out, vec!["Total".into()], TOTAL_KEY);
1633 }
1634 for &leaf in &row_leaves {
1635 let mut labels = node_path(&res.row_nodes, leaf);
1636 while labels.len() < res.row_depth {
1637 labels.push(String::new());
1638 }
1639 emit_line(&mut out, labels, leaf);
1640 }
1641 if self.config.show_row_grand_total && !row_leaves.is_empty() {
1642 let mut labels = vec!["Grand Total".to_owned()];
1643 while labels.len() < res.row_depth.max(1) {
1644 labels.push(String::new());
1645 }
1646 emit_line(&mut out, labels, TOTAL_KEY);
1647 }
1648 out
1649 }
1650}
1651
1652fn default_value_format() -> ResolvedColumnFormat {
1653 crate::config::GridConfig::default().resolve(usize::MAX, ColumnKind::Decimal)
1654}
1655
1656fn norm_rect(r1: usize, c1: usize, r2: usize, c2: usize) -> (usize, usize, usize, usize) {
1657 (r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2))
1658}
1659
1660fn push_csv_line(out: &mut String, fields: &[String]) {
1661 for (i, f) in fields.iter().enumerate() {
1662 if i > 0 {
1663 out.push(',');
1664 }
1665 if f.contains(',') || f.contains('"') || f.contains('\n') {
1666 out.push('"');
1667 out.push_str(&f.replace('"', "\"\""));
1668 out.push('"');
1669 } else {
1670 out.push_str(f);
1671 }
1672 }
1673 out.push('\n');
1674}
1675
1676pub(crate) fn node_path(nodes: &[PivotNode], node: usize) -> Vec<String> {
1678 let mut path = Vec::new();
1679 let mut current = Some(node);
1680 while let Some(id) = current {
1681 path.push(nodes[id].label.clone());
1682 current = nodes[id].parent;
1683 }
1684 path.reverse();
1685 path
1686}
1687
1688pub(crate) fn path_components(
1691 nodes: &[PivotNode],
1692 fields: &[usize],
1693 columns: &[Column],
1694 blank_label: &str,
1695 key: usize,
1696) -> Vec<PivotPathComponent> {
1697 if key == TOTAL_KEY || key >= nodes.len() {
1698 return Vec::new();
1699 }
1700 let mut chain = Vec::new();
1701 let mut current = Some(key);
1702 while let Some(id) = current {
1703 chain.push(id);
1704 current = nodes[id].parent;
1705 }
1706 chain.reverse();
1707 chain
1708 .into_iter()
1709 .map(|id| {
1710 let node = &nodes[id];
1711 let field_index = fields.get(node.depth).copied().unwrap_or(usize::MAX);
1712 PivotPathComponent {
1713 field_index,
1714 field_name: columns
1715 .get(field_index)
1716 .map(|c| c.name.clone())
1717 .unwrap_or_default(),
1718 label: node.label.clone(),
1719 group_value: node.sort_key.clone(),
1720 is_blank: node.label == blank_label,
1721 }
1722 })
1723 .collect()
1724}
1725
1726pub(crate) fn rows_matching_path(
1730 rows: &[Vec<CellValue>],
1731 formats: &[ResolvedColumnFormat],
1732 blank_label: &str,
1733 base: &[usize],
1734 constraints: &[(usize, String)],
1735) -> Vec<usize> {
1736 if constraints.is_empty() {
1737 return base.to_vec();
1738 }
1739 base.iter()
1740 .copied()
1741 .filter(|&r| {
1742 constraints.iter().all(|(field, label)| {
1743 let cell = rows
1744 .get(r)
1745 .and_then(|row| row.get(*field))
1746 .unwrap_or(&CellValue::None);
1747 let cell_label = if matches!(cell, CellValue::None) {
1748 blank_label.to_owned()
1749 } else {
1750 format_cell(cell, &formats[*field]).0
1751 };
1752 cell_label == *label
1753 })
1754 })
1755 .collect()
1756}
1757
1758pub(crate) fn drill_filter_set(label: &str, blank_label: &str) -> HashSet<String> {
1763 if label == blank_label {
1764 HashSet::from([String::new(), label.to_owned()])
1765 } else {
1766 HashSet::from([label.to_owned()])
1767 }
1768}
1769
1770fn all_group_paths(nodes: &[PivotNode]) -> HashSet<Vec<String>> {
1772 nodes
1773 .iter()
1774 .enumerate()
1775 .filter(|(_, n)| !n.is_leaf())
1776 .map(|(id, _)| node_path(nodes, id))
1777 .collect()
1778}
1779
1780pub(crate) fn flatten_rows(
1782 result: &PivotResult,
1783 collapsed: &HashSet<Vec<String>>,
1784 config: &PivotConfig,
1785) -> Vec<VisibleRow> {
1786 let mut out = Vec::new();
1787 if result.row_depth == 0 {
1788 if !result.values.is_empty() {
1790 out.push(VisibleRow {
1791 key: TOTAL_KEY,
1792 depth: 0,
1793 kind: VisibleRowKind::Leaf,
1794 zebra: false,
1795 });
1796 }
1797 return out;
1798 }
1799 fn walk(
1800 result: &PivotResult,
1801 collapsed: &HashSet<Vec<String>>,
1802 id: usize,
1803 path: &mut Vec<String>,
1804 zebra: &mut bool,
1805 out: &mut Vec<VisibleRow>,
1806 ) {
1807 let node = &result.row_nodes[id];
1808 path.push(node.label.clone());
1809 if node.is_leaf() {
1810 out.push(VisibleRow {
1811 key: id,
1812 depth: node.depth,
1813 kind: VisibleRowKind::Leaf,
1814 zebra: *zebra,
1815 });
1816 *zebra = !*zebra;
1817 } else if collapsed.contains(path) {
1818 *zebra = false;
1819 out.push(VisibleRow {
1820 key: id,
1821 depth: node.depth,
1822 kind: VisibleRowKind::GroupHeader { expanded: false },
1823 zebra: false,
1824 });
1825 } else {
1826 *zebra = false;
1828 out.push(VisibleRow {
1829 key: id,
1830 depth: node.depth,
1831 kind: VisibleRowKind::GroupHeader { expanded: true },
1832 zebra: false,
1833 });
1834 for &child in &node.children {
1835 walk(result, collapsed, child, path, zebra, out);
1836 }
1837 *zebra = false;
1838 }
1839 path.pop();
1840 }
1841 let mut path = Vec::new();
1842 let mut zebra = false;
1843 for &root in &result.row_roots {
1844 walk(result, collapsed, root, &mut path, &mut zebra, &mut out);
1845 }
1846 if config.show_row_grand_total && !out.is_empty() {
1847 out.push(VisibleRow {
1848 key: TOTAL_KEY,
1849 depth: 0,
1850 kind: VisibleRowKind::GrandTotal,
1851 zebra: false,
1852 });
1853 }
1854 out
1855}
1856
1857pub(crate) fn flatten_cols(
1859 result: &PivotResult,
1860 collapsed: &HashSet<Vec<String>>,
1861 config: &PivotConfig,
1862) -> Vec<VisibleCol> {
1863 let mut out = Vec::new();
1864 if result.col_depth == 0 {
1865 if !result.values.is_empty() {
1866 out.push(VisibleCol {
1867 key: TOTAL_KEY,
1868 depth: 0,
1869 kind: VisibleColKind::Leaf,
1870 });
1871 }
1872 return out;
1873 }
1874 fn walk(
1875 result: &PivotResult,
1876 collapsed: &HashSet<Vec<String>>,
1877 config: &PivotConfig,
1878 id: usize,
1879 path: &mut Vec<String>,
1880 out: &mut Vec<VisibleCol>,
1881 ) {
1882 let node = &result.col_nodes[id];
1883 path.push(node.label.clone());
1884 if node.is_leaf() {
1885 out.push(VisibleCol {
1886 key: id,
1887 depth: node.depth,
1888 kind: VisibleColKind::Leaf,
1889 });
1890 } else if collapsed.contains(path) {
1891 out.push(VisibleCol {
1892 key: id,
1893 depth: node.depth,
1894 kind: VisibleColKind::Collapsed,
1895 });
1896 } else {
1897 for &child in &node.children {
1898 walk(result, collapsed, config, child, path, out);
1899 }
1900 if config.show_column_subtotals {
1901 out.push(VisibleCol {
1902 key: id,
1903 depth: node.depth,
1904 kind: VisibleColKind::Subtotal,
1905 });
1906 }
1907 }
1908 path.pop();
1909 }
1910 let mut path = Vec::new();
1911 for &root in &result.col_roots {
1912 walk(result, collapsed, config, root, &mut path, &mut out);
1913 }
1914 if config.show_column_grand_total && !out.is_empty() {
1915 out.push(VisibleCol {
1916 key: TOTAL_KEY,
1917 depth: 0,
1918 kind: VisibleColKind::GrandTotal,
1919 });
1920 }
1921 out
1922}
1923
1924fn sort_axis_by_key(nodes: &mut [PivotNode], roots: &mut [usize], descending: bool) {
1926 let keys: Vec<CellValue> = nodes.iter().map(|n| n.sort_key.clone()).collect();
1927 sort_axis_by(nodes, roots, &keys, descending);
1928}
1929
1930fn sort_axis_by(
1932 nodes: &mut [PivotNode],
1933 roots: &mut [usize],
1934 keys: &[CellValue],
1935 descending: bool,
1936) {
1937 let cmp = |a: &usize, b: &usize| {
1938 let ord = compare_cells(&keys[*a], &keys[*b]);
1939 if descending {
1940 ord.reverse()
1941 } else {
1942 ord
1943 }
1944 };
1945 roots.sort_by(cmp);
1946 for node in nodes.iter_mut() {
1947 let mut children = std::mem::take(&mut node.children);
1948 children.sort_by(cmp);
1949 node.children = children;
1950 }
1951}
1952
1953#[cfg(test)]
1954#[allow(clippy::unwrap_used)]
1955mod tests {
1956 use super::*;
1957 use crate::config::GridConfig;
1958 use crate::data::ColumnKind;
1959 use CellValue::{Decimal, Integer, Text};
1960
1961 fn fixture_result(config: &PivotConfig) -> PivotResult {
1962 let columns = vec![
1963 Column::new("region", ColumnKind::Text, 100.0),
1964 Column::new("product", ColumnKind::Text, 100.0),
1965 Column::new("year", ColumnKind::Integer, 80.0),
1966 Column::new("amount", ColumnKind::Decimal, 100.0),
1967 ];
1968 let r = |region: &str, product: &str, year: i64, amount: f64| {
1969 vec![
1970 Text(region.into()),
1971 Text(product.into()),
1972 Integer(year),
1973 Decimal(amount),
1974 ]
1975 };
1976 let rows = vec![
1977 r("Europe", "Widget", 2023, 10.0),
1978 r("Europe", "Widget", 2024, 20.0),
1979 r("Europe", "Gadget", 2023, 5.0),
1980 r("Asia", "Widget", 2023, 7.0),
1981 r("Asia", "Gadget", 2024, 3.0),
1982 ];
1983 let formats = GridConfig::default().resolve_all(&columns);
1984 let all: Vec<usize> = (0..rows.len()).collect();
1985 compute_pivot(&columns, &rows, &all, config, &formats)
1986 }
1987
1988 fn two_level_config() -> PivotConfig {
1989 PivotConfig {
1990 row_fields: vec![0, 1],
1991 column_fields: vec![2],
1992 value_field: Some(3),
1993 ..PivotConfig::default()
1994 }
1995 }
1996
1997 #[test]
1998 fn flatten_rows_expanded_lists_headers_and_leaves() {
1999 let cfg = two_level_config();
2000 let result = fixture_result(&cfg);
2001 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2002 assert_eq!(rows.len(), 7);
2004 assert_eq!(rows[0].kind, VisibleRowKind::GroupHeader { expanded: true });
2005 assert_eq!(rows[0].depth, 0);
2006 assert_eq!(rows[1].kind, VisibleRowKind::Leaf);
2007 assert_eq!(rows[1].depth, 1);
2008 assert_eq!(rows[6].kind, VisibleRowKind::GrandTotal);
2009 }
2010
2011 #[test]
2012 fn flatten_rows_collapse_hides_children() {
2013 let cfg = two_level_config();
2014 let result = fixture_result(&cfg);
2015 let mut collapsed = HashSet::new();
2016 collapsed.insert(vec!["Asia".to_owned()]);
2017 let rows = flatten_rows(&result, &collapsed, &cfg);
2018 assert_eq!(rows.len(), 5);
2020 assert_eq!(
2021 rows[0].kind,
2022 VisibleRowKind::GroupHeader { expanded: false }
2023 );
2024 assert_eq!(rows[1].kind, VisibleRowKind::GroupHeader { expanded: true });
2025 }
2026
2027 #[test]
2028 fn flatten_rows_without_grand_total() {
2029 let mut cfg = two_level_config();
2030 cfg.show_row_grand_total = false;
2031 let result = fixture_result(&cfg);
2032 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
2033 assert!(rows.iter().all(|r| r.kind != VisibleRowKind::GrandTotal));
2034 }
2035
2036 #[test]
2037 fn flatten_cols_leaves_plus_grand_total() {
2038 let cfg = two_level_config();
2039 let result = fixture_result(&cfg);
2040 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2041 assert_eq!(cols.len(), 3);
2043 assert_eq!(cols[0].kind, VisibleColKind::Leaf);
2044 assert_eq!(cols[2].kind, VisibleColKind::GrandTotal);
2045 }
2046
2047 #[test]
2048 fn flatten_cols_collapsed_group_is_single_column() {
2049 let mut cfg = two_level_config();
2050 cfg.row_fields = vec![0];
2052 cfg.column_fields = vec![2, 1];
2053 let result = fixture_result(&cfg);
2054 let first_year = result.col_roots[0];
2057 let mut collapsed = HashSet::new();
2058 collapsed.insert(vec![result.col_nodes[first_year].label.clone()]);
2059 let cols = flatten_cols(&result, &collapsed, &cfg);
2060 assert_eq!(cols[0].kind, VisibleColKind::Collapsed);
2062 assert!(cols.len() >= 3);
2063 }
2064
2065 #[test]
2066 fn flatten_cols_subtotal_columns_follow_expanded_groups() {
2067 let mut cfg = two_level_config();
2068 cfg.row_fields = vec![0];
2069 cfg.column_fields = vec![2, 1];
2070 cfg.show_column_subtotals = true;
2071 let result = fixture_result(&cfg);
2072 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2073 let subtotal_count = cols
2074 .iter()
2075 .filter(|c| c.kind == VisibleColKind::Subtotal)
2076 .count();
2077 assert_eq!(subtotal_count, 2); let first_sub = cols
2080 .iter()
2081 .position(|c| c.kind == VisibleColKind::Subtotal)
2082 .unwrap();
2083 assert!(first_sub > 0);
2084 assert_eq!(cols[first_sub - 1].kind, VisibleColKind::Leaf);
2085 }
2086
2087 #[test]
2088 fn flatten_cols_no_column_fields_yields_single_value_column() {
2089 let mut cfg = two_level_config();
2090 cfg.column_fields = vec![];
2091 let result = fixture_result(&cfg);
2092 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
2093 assert_eq!(cols.len(), 1);
2094 assert_eq!(cols[0].key, TOTAL_KEY);
2095 }
2096
2097 #[test]
2098 fn flatten_empty_result_is_empty() {
2099 let cfg = PivotConfig::default();
2100 let result = PivotResult::default();
2101 assert!(flatten_rows(&result, &HashSet::new(), &cfg).is_empty());
2102 assert!(flatten_cols(&result, &HashSet::new(), &cfg).is_empty());
2103 }
2104
2105 #[test]
2106 fn sort_axis_descending_reverses_roots_and_children() {
2107 let cfg = two_level_config();
2108 let mut result = fixture_result(&cfg);
2109 let labels = |result: &PivotResult| -> Vec<String> {
2110 result
2111 .row_roots
2112 .iter()
2113 .map(|&r| result.row_nodes[r].label.clone())
2114 .collect()
2115 };
2116 assert_eq!(labels(&result), vec!["Asia", "Europe"]);
2117 let mut roots = result.row_roots.clone();
2118 sort_axis_by_key(&mut result.row_nodes, &mut roots, true);
2119 result.row_roots = roots;
2120 assert_eq!(labels(&result), vec!["Europe", "Asia"]);
2121 let asia = result.row_roots[1];
2122 let child_labels: Vec<&str> = result.row_nodes[asia]
2123 .children
2124 .iter()
2125 .map(|&c| result.row_nodes[c].label.as_str())
2126 .collect();
2127 assert_eq!(child_labels, vec!["Widget", "Gadget"]);
2128 }
2129
2130 #[test]
2131 fn sort_axis_by_values_orders_missing_first_ascending() {
2132 let cfg = two_level_config();
2133 let mut result = fixture_result(&cfg);
2134 let y2024 = result
2136 .col_roots
2137 .iter()
2138 .copied()
2139 .find(|&c| result.col_nodes[c].sort_key == Integer(2024))
2140 .unwrap();
2141 let keys: Vec<CellValue> = (0..result.row_nodes.len())
2142 .map(|id| {
2143 result
2144 .values
2145 .get(&(id, y2024))
2146 .cloned()
2147 .unwrap_or(CellValue::None)
2148 })
2149 .collect();
2150 let mut roots = result.row_roots.clone();
2151 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, false);
2152 let labels: Vec<&str> = roots
2153 .iter()
2154 .map(|&r| result.row_nodes[r].label.as_str())
2155 .collect();
2156 assert_eq!(labels, vec!["Asia", "Europe"]);
2157 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, true);
2158 let labels: Vec<&str> = roots
2159 .iter()
2160 .map(|&r| result.row_nodes[r].label.as_str())
2161 .collect();
2162 assert_eq!(labels, vec!["Europe", "Asia"]);
2163 }
2164
2165 #[test]
2166 fn node_path_walks_to_root() {
2167 let cfg = two_level_config();
2168 let result = fixture_result(&cfg);
2169 let europe = result
2170 .row_nodes
2171 .iter()
2172 .position(|n| n.label == "Europe")
2173 .unwrap();
2174 let widget = result.row_nodes[europe]
2175 .children
2176 .iter()
2177 .copied()
2178 .find(|&c| result.row_nodes[c].label == "Widget")
2179 .unwrap();
2180 assert_eq!(
2181 node_path(&result.row_nodes, widget),
2182 vec!["Europe".to_owned(), "Widget".to_owned()]
2183 );
2184 }
2185
2186 #[test]
2187 fn all_group_paths_lists_only_non_leaves() {
2188 let cfg = two_level_config();
2189 let result = fixture_result(&cfg);
2190 let paths = all_group_paths(&result.row_nodes);
2191 assert_eq!(paths.len(), 2); assert!(paths.contains(&vec!["Asia".to_owned()]));
2193 }
2194
2195 #[test]
2196 fn csv_line_quotes_fields_with_commas_and_quotes() {
2197 let mut out = String::new();
2198 push_csv_line(
2199 &mut out,
2200 &["a,b".to_owned(), "plain".to_owned(), "q\"q".to_owned()],
2201 );
2202 assert_eq!(out, "\"a,b\",plain,\"q\"\"q\"\n");
2203 }
2204
2205 fn fixture_columns() -> Vec<Column> {
2206 vec![
2207 Column::new("region", ColumnKind::Text, 100.0),
2208 Column::new("product", ColumnKind::Text, 100.0),
2209 Column::new("year", ColumnKind::Integer, 80.0),
2210 Column::new("amount", ColumnKind::Decimal, 100.0),
2211 ]
2212 }
2213
2214 fn fixture_rows() -> Vec<Vec<CellValue>> {
2215 let r = |region: &str, product: &str, year: i64, amount: f64| {
2216 vec![
2217 Text(region.into()),
2218 Text(product.into()),
2219 Integer(year),
2220 Decimal(amount),
2221 ]
2222 };
2223 vec![
2224 r("Europe", "Widget", 2023, 10.0),
2225 r("Europe", "Widget", 2024, 20.0),
2226 r("Europe", "Gadget", 2023, 5.0),
2227 r("Asia", "Widget", 2023, 7.0),
2228 r("Asia", "Gadget", 2024, 3.0),
2229 ]
2230 }
2231
2232 #[test]
2233 fn path_components_walk_root_to_leaf_with_field_metadata() {
2234 let cfg = two_level_config();
2235 let result = fixture_result(&cfg);
2236 let columns = fixture_columns();
2237 let europe = result
2238 .row_nodes
2239 .iter()
2240 .position(|n| n.label == "Europe")
2241 .unwrap();
2242 let widget = result.row_nodes[europe]
2243 .children
2244 .iter()
2245 .copied()
2246 .find(|&c| result.row_nodes[c].label == "Widget")
2247 .unwrap();
2248 let path = path_components(
2249 &result.row_nodes,
2250 &cfg.row_fields,
2251 &columns,
2252 "(blank)",
2253 widget,
2254 );
2255 assert_eq!(path.len(), 2);
2256 assert_eq!(path[0].field_name, "region");
2257 assert_eq!(path[0].label, "Europe");
2258 assert_eq!(path[0].field_index, 0);
2259 assert_eq!(path[1].field_name, "product");
2260 assert_eq!(path[1].label, "Widget");
2261 assert!(!path[1].is_blank);
2262 assert!(path_components(
2264 &result.row_nodes,
2265 &cfg.row_fields,
2266 &columns,
2267 "(blank)",
2268 TOTAL_KEY
2269 )
2270 .is_empty());
2271 }
2272
2273 #[test]
2274 fn rows_matching_path_selects_only_driving_rows() {
2275 let rows = fixture_rows();
2276 let columns = fixture_columns();
2277 let formats = GridConfig::default().resolve_all(&columns);
2278 let base: Vec<usize> = (0..rows.len()).collect();
2279 let constraints = vec![(0, "Europe".to_owned()), (1, "Widget".to_owned())];
2281 assert_eq!(
2282 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2283 vec![0, 1]
2284 );
2285 assert_eq!(
2287 rows_matching_path(&rows, &formats, "(blank)", &base, &[]),
2288 base
2289 );
2290 assert_eq!(
2292 rows_matching_path(&rows, &formats, "(blank)", &[1, 2, 3], &constraints),
2293 vec![1]
2294 );
2295 }
2296
2297 #[test]
2298 fn rows_matching_path_buckets_nulls_under_blank_label() {
2299 let mut rows = fixture_rows();
2300 rows.push(vec![
2301 CellValue::None,
2302 Text("Widget".into()),
2303 Integer(2023),
2304 Decimal(2.0),
2305 ]);
2306 let columns = fixture_columns();
2307 let formats = GridConfig::default().resolve_all(&columns);
2308 let base: Vec<usize> = (0..rows.len()).collect();
2309 let constraints = vec![(0, "(blank)".to_owned())];
2310 assert_eq!(
2311 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2312 vec![5]
2313 );
2314 }
2315
2316 #[test]
2317 fn drill_filter_set_maps_blank_to_empty_formatted_value() {
2318 let set = drill_filter_set("(blank)", "(blank)");
2319 assert!(set.contains(""));
2320 assert!(set.contains("(blank)"));
2321 let set = drill_filter_set("Europe", "(blank)");
2322 assert_eq!(set, HashSet::from(["Europe".to_owned()]));
2323 }
2324}