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