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 RowChevron {
120 row: usize,
122 },
123 ColChevron {
125 col: usize,
127 depth: usize,
129 },
130 Cell {
132 row: usize,
134 col: usize,
136 },
137 VScrollbar,
139 HScrollbar,
141}
142
143#[derive(Clone, Debug)]
145pub struct PivotFilterPopover {
146 pub field: usize,
148 pub rows: Vec<FilterValueRow>,
150 pub anchor: Point<Pixels>,
152}
153
154impl PivotFilterPopover {
155 #[must_use]
157 pub fn all_checked(&self) -> bool {
158 !self.rows.is_empty() && self.rows.iter().all(|r| r.checked)
159 }
160}
161
162#[derive(Clone, Debug)]
165pub(crate) struct PivotMenu {
166 pub(crate) anchor: Point<Pixels>,
168 pub(crate) items: Vec<PivotMenuItem>,
170 pub(crate) hovered: Option<usize>,
172 pub(crate) request: PivotContextMenuRequest,
174 pub(crate) drill: Vec<(usize, HashSet<String>)>,
177}
178
179pub(crate) const ROW_INDENT: f32 = 16.0;
181pub(crate) const CHEVRON_SIZE: f32 = 14.0;
183
184pub struct PivotState {
186 pub config: PivotConfig,
190 pub result: Arc<PivotResult>,
192 pub(crate) source_rows: Arc<Vec<Vec<CellValue>>>,
194 pub(crate) source_columns: Vec<Column>,
196 pub(crate) resolved_formats: Vec<ResolvedColumnFormat>,
198 pub(crate) value_fmt: ResolvedColumnFormat,
200
201 pub(crate) visible_rows: Arc<Vec<VisibleRow>>,
204 pub(crate) visible_cols: Arc<Vec<VisibleCol>>,
206 pub(crate) collapsed_row_paths: HashSet<Vec<String>>,
209 pub(crate) collapsed_col_paths: HashSet<Vec<String>>,
211 pub sort: Option<(PivotSortKey, SortDirection)>,
213 pub(crate) filter_values: HashMap<usize, HashSet<String>>,
216 pub(crate) filter_popover: Option<PivotFilterPopover>,
218 pub agg_menu_open: bool,
220 pub(crate) context_menu_provider: Option<PivotContextMenuProviderHandle>,
222 pub(crate) menu: Option<PivotMenu>,
224 pub(crate) pending_drill_down: Option<Vec<(usize, HashSet<String>)>>,
229
230 pub selection: Option<(usize, usize, usize, usize)>,
233 pub(crate) select_anchor: Option<(usize, usize)>,
234 pub(crate) is_selecting: bool,
235 pub hover_hit: Option<PivotHitResult>,
237 pub(crate) scrollbar_drag: Option<ScrollbarAxis>,
238
239 pub theme: GridTheme,
241 pub(crate) key_bindings: KeyBindings,
242 pub scroll_handle: ScrollHandle,
244 pub focus_handle: FocusHandle,
246 pub bounds: Bounds<Pixels>,
248 pub(crate) window_viewport: Size<Pixels>,
249 pub row_height: f32,
251 pub header_row_height: f32,
253 pub row_header_width: f32,
255 pub value_col_width: f32,
257 pub font_size: f32,
259 pub char_width: f32,
261}
262
263impl PivotState {
264 #[must_use]
267 pub fn new(
268 source_columns: Vec<Column>,
269 source_rows: Arc<Vec<Vec<CellValue>>>,
270 resolved_formats: Vec<ResolvedColumnFormat>,
271 config: PivotConfig,
272 key_bindings: KeyBindings,
273 focus_handle: FocusHandle,
274 ) -> Self {
275 let mut state = Self {
276 config,
277 result: Arc::new(PivotResult::default()),
278 source_rows,
279 source_columns,
280 resolved_formats,
281 value_fmt: default_value_format(),
282 visible_rows: Arc::new(Vec::new()),
283 visible_cols: Arc::new(Vec::new()),
284 collapsed_row_paths: HashSet::new(),
285 collapsed_col_paths: HashSet::new(),
286 sort: None,
287 filter_values: HashMap::new(),
288 filter_popover: None,
289 agg_menu_open: false,
290 context_menu_provider: None,
291 menu: None,
292 pending_drill_down: None,
293 selection: None,
294 select_anchor: None,
295 is_selecting: false,
296 hover_hit: None,
297 scrollbar_drag: None,
298 theme: GridTheme::default(),
299 key_bindings,
300 scroll_handle: ScrollHandle::new(),
301 focus_handle,
302 bounds: Bounds::default(),
303 window_viewport: Size::default(),
304 row_height: 24.0,
305 header_row_height: 26.0,
306 row_header_width: 220.0,
307 value_col_width: 140.0,
308 font_size: 13.0,
309 char_width: 7.9,
310 };
311 state.recompute();
312 state
313 }
314
315 pub fn set_source(&mut self, columns: Vec<Column>, rows: Arc<Vec<Vec<CellValue>>>) {
318 self.source_columns = columns;
319 self.source_rows = rows;
320 self.recompute();
321 }
322
323 #[must_use]
325 pub fn source_differs(&self, rows: &Arc<Vec<Vec<CellValue>>>) -> bool {
326 !Arc::ptr_eq(&self.source_rows, rows)
327 }
328
329 #[must_use]
331 pub fn source_columns(&self) -> &[Column] {
332 &self.source_columns
333 }
334
335 pub fn recompute(&mut self) {
338 self.config.clamp_to_columns(self.source_columns.len());
339 let filter_fields = self.config.filter_fields.clone();
340 self.filter_values
341 .retain(|field, _| filter_fields.contains(field));
342
343 if self.config.is_ready() {
344 let included = self.filtered_source_rows();
345 self.result = Arc::new(compute_pivot(
346 &self.source_columns,
347 &self.source_rows,
348 &included,
349 &self.config,
350 &self.resolved_formats,
351 ));
352 } else {
353 self.result = Arc::new(PivotResult::default());
354 }
355 self.value_fmt = self.build_value_format();
356 self.resort();
357 }
358
359 fn filtered_source_rows(&self) -> Vec<usize> {
361 let active: Vec<(usize, &HashSet<String>)> = self
362 .config
363 .filter_fields
364 .iter()
365 .filter_map(|&f| self.filter_values.get(&f).map(|set| (f, set)))
366 .collect();
367 if active.is_empty() {
368 return (0..self.source_rows.len()).collect();
369 }
370 (0..self.source_rows.len())
371 .filter(|&r| {
372 active.iter().all(|(field, allowed)| {
373 let cell = self.source_rows[r].get(*field).unwrap_or(&CellValue::None);
374 let label = format_cell(cell, &self.resolved_formats[*field]).0;
375 allowed.contains(&label)
376 })
377 })
378 .collect()
379 }
380
381 fn build_value_format(&self) -> ResolvedColumnFormat {
386 let mut fmt = self
387 .config
388 .value_field
389 .and_then(|f| self.resolved_formats.get(f).cloned())
390 .unwrap_or_else(default_value_format);
391 match self.config.aggregation {
392 AggregationFn::Count => {
393 fmt.kind = ColumnKind::Integer;
394 fmt.number = NumberFormat {
395 decimals: 0,
396 ..fmt.number
397 };
398 }
399 AggregationFn::Avg => fmt.kind = ColumnKind::Decimal,
400 AggregationFn::Sum | AggregationFn::Min | AggregationFn::Max => {}
401 }
402 if let Some(over) = self.config.value_format {
403 fmt.number = over;
404 }
405 fmt
406 }
407
408 #[must_use]
410 pub fn value_format(&self) -> &ResolvedColumnFormat {
411 &self.value_fmt
412 }
413
414 pub(crate) fn rebuild_visible(&mut self) {
421 self.visible_rows = Arc::new(flatten_rows(
422 &self.result,
423 &self.collapsed_row_paths,
424 &self.config,
425 ));
426 self.visible_cols = Arc::new(flatten_cols(
427 &self.result,
428 &self.collapsed_col_paths,
429 &self.config,
430 ));
431 self.clamp_selection();
432 self.clamp_scroll_to_bounds();
433 }
434
435 #[must_use]
437 pub fn visible_rows(&self) -> &[VisibleRow] {
438 &self.visible_rows
439 }
440
441 #[must_use]
443 pub fn visible_cols(&self) -> &[VisibleCol] {
444 &self.visible_cols
445 }
446
447 pub fn toggle_row_group(&mut self, node: usize) {
450 if node >= self.result.row_nodes.len() {
451 return;
452 }
453 let path = node_path(&self.result.row_nodes, node);
454 if !self.collapsed_row_paths.remove(&path) {
455 self.collapsed_row_paths.insert(path);
456 }
457 self.rebuild_visible();
458 }
459
460 pub fn toggle_col_group(&mut self, node: usize) {
462 if node >= self.result.col_nodes.len() {
463 return;
464 }
465 let path = node_path(&self.result.col_nodes, node);
466 if !self.collapsed_col_paths.remove(&path) {
467 self.collapsed_col_paths.insert(path);
468 }
469 self.rebuild_visible();
470 }
471
472 pub fn collapse_all_rows(&mut self) {
474 self.collapsed_row_paths = all_group_paths(&self.result.row_nodes);
475 self.rebuild_visible();
476 }
477
478 pub fn expand_all_rows(&mut self) {
480 self.collapsed_row_paths.clear();
481 self.rebuild_visible();
482 }
483
484 pub fn collapse_all_cols(&mut self) {
486 self.collapsed_col_paths = all_group_paths(&self.result.col_nodes);
487 self.rebuild_visible();
488 }
489
490 pub fn expand_all_cols(&mut self) {
492 self.collapsed_col_paths.clear();
493 self.rebuild_visible();
494 }
495
496 pub fn cycle_sort_by_column(&mut self, col_key: usize) {
503 self.sort = match self.sort {
504 Some((PivotSortKey::RowsByColumn(k), SortDirection::Ascending)) if k == col_key => {
505 Some((
506 PivotSortKey::RowsByColumn(col_key),
507 SortDirection::Descending,
508 ))
509 }
510 Some((PivotSortKey::RowsByColumn(k), SortDirection::Descending)) if k == col_key => {
511 None
512 }
513 _ => Some((
514 PivotSortKey::RowsByColumn(col_key),
515 SortDirection::Ascending,
516 )),
517 };
518 self.resort();
519 }
520
521 pub fn cycle_label_sort(&mut self) {
524 self.sort = match self.sort {
525 Some((PivotSortKey::RowLabel, SortDirection::Ascending)) => {
526 Some((PivotSortKey::RowLabel, SortDirection::Descending))
527 }
528 Some((PivotSortKey::RowLabel, SortDirection::Descending)) => None,
529 _ => Some((PivotSortKey::RowLabel, SortDirection::Ascending)),
530 };
531 self.resort();
532 }
533
534 pub fn cycle_col_label_sort(&mut self) {
536 self.sort = match self.sort {
537 Some((PivotSortKey::ColLabel, SortDirection::Ascending)) => {
538 Some((PivotSortKey::ColLabel, SortDirection::Descending))
539 }
540 Some((PivotSortKey::ColLabel, SortDirection::Descending)) => None,
541 _ => Some((PivotSortKey::ColLabel, SortDirection::Ascending)),
542 };
543 self.resort();
544 }
545
546 pub(crate) fn resort(&mut self) {
550 let result = Arc::make_mut(&mut self.result);
551 sort_axis_by_key(&mut result.row_nodes, &mut result.row_roots, false);
554 sort_axis_by_key(&mut result.col_nodes, &mut result.col_roots, false);
555 match self.sort {
556 None => {}
557 Some((PivotSortKey::RowLabel, dir)) => {
558 sort_axis_by_key(
559 &mut result.row_nodes,
560 &mut result.row_roots,
561 dir == SortDirection::Descending,
562 );
563 }
564 Some((PivotSortKey::ColLabel, dir)) => {
565 sort_axis_by_key(
566 &mut result.col_nodes,
567 &mut result.col_roots,
568 dir == SortDirection::Descending,
569 );
570 }
571 Some((PivotSortKey::RowsByColumn(col_key), dir)) => {
572 let keys: Vec<CellValue> = (0..result.row_nodes.len())
573 .map(|id| {
574 result
575 .values
576 .get(&(id, col_key))
577 .cloned()
578 .unwrap_or(CellValue::None)
579 })
580 .collect();
581 sort_axis_by(
582 &mut result.row_nodes,
583 &mut result.row_roots,
584 &keys,
585 dir == SortDirection::Descending,
586 );
587 }
588 }
589 self.rebuild_visible();
590 }
591
592 pub fn open_filter_popover(&mut self, field: usize, anchor: Point<Pixels>) {
599 if field >= self.source_columns.len() {
600 return;
601 }
602 let fmt = &self.resolved_formats[field];
603 let allowed = self.filter_values.get(&field);
604 let mut seen: HashSet<String> = HashSet::new();
605 let mut pairs: Vec<(String, CellValue)> = Vec::new();
606 for row in self.source_rows.iter() {
607 let cell = row.get(field).unwrap_or(&CellValue::None);
608 let label = format_cell(cell, fmt).0;
609 if seen.insert(label.clone()) {
610 pairs.push((label, cell.clone()));
611 }
612 }
613 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
614 let rows = pairs
615 .into_iter()
616 .map(|(label, _)| {
617 let checked = allowed.is_none_or(|set| set.contains(&label));
618 FilterValueRow { label, checked }
619 })
620 .collect();
621 self.filter_popover = Some(PivotFilterPopover {
622 field,
623 rows,
624 anchor,
625 });
626 }
627
628 #[must_use]
630 pub fn filter_popover(&self) -> Option<&PivotFilterPopover> {
631 self.filter_popover.as_ref()
632 }
633
634 pub fn toggle_filter_popover_value(&mut self, index: usize) {
636 if let Some(p) = &mut self.filter_popover {
637 if let Some(row) = p.rows.get_mut(index) {
638 row.checked = !row.checked;
639 }
640 }
641 self.apply_filter_popover();
642 }
643
644 pub fn toggle_filter_popover_select_all(&mut self) {
646 if let Some(p) = &mut self.filter_popover {
647 let target = !p.all_checked();
648 for row in &mut p.rows {
649 row.checked = target;
650 }
651 }
652 self.apply_filter_popover();
653 }
654
655 pub fn apply_filter_popover(&mut self) {
658 let Some(p) = &self.filter_popover else {
659 return;
660 };
661 let field = p.field;
662 if p.all_checked() {
663 self.filter_values.remove(&field);
664 } else {
665 self.filter_values.insert(
666 field,
667 p.rows
668 .iter()
669 .filter(|r| r.checked)
670 .map(|r| r.label.clone())
671 .collect(),
672 );
673 }
674 self.recompute();
675 }
676
677 pub fn close_filter_popover(&mut self) {
679 self.filter_popover = None;
680 }
681
682 pub fn clear_filter(&mut self, field: usize) {
684 if self.filter_values.remove(&field).is_some() {
685 self.recompute();
686 }
687 if let Some(p) = &mut self.filter_popover {
688 if p.field == field {
689 for row in &mut p.rows {
690 row.checked = true;
691 }
692 }
693 }
694 }
695
696 #[must_use]
699 pub fn filter_active(&self, field: usize) -> bool {
700 self.filter_values.contains_key(&field)
701 }
702
703 pub fn set_context_menu_provider(
711 &mut self,
712 provider: impl crate::pivot::context_menu::PivotContextMenuProvider + 'static,
713 ) {
714 self.context_menu_provider = Some(PivotContextMenuProviderHandle::new(provider));
715 }
716
717 #[must_use]
719 pub fn row_path_components(&self, row_key: usize) -> Vec<PivotPathComponent> {
720 path_components(
721 &self.result.row_nodes,
722 &self.config.row_fields,
723 &self.source_columns,
724 &self.config.blank_label,
725 row_key,
726 )
727 }
728
729 #[must_use]
731 pub fn col_path_components(&self, col_key: usize) -> Vec<PivotPathComponent> {
732 path_components(
733 &self.result.col_nodes,
734 &self.config.column_fields,
735 &self.source_columns,
736 &self.config.blank_label,
737 col_key,
738 )
739 }
740
741 #[must_use]
746 pub fn source_rows_for(&self, row_key: usize, col_key: usize) -> Vec<usize> {
747 let constraints: Vec<(usize, String)> = self
748 .row_path_components(row_key)
749 .into_iter()
750 .chain(self.col_path_components(col_key))
751 .map(|c| (c.field_index, c.label))
752 .collect();
753 rows_matching_path(
754 &self.source_rows,
755 &self.resolved_formats,
756 &self.config.blank_label,
757 &self.filtered_source_rows(),
758 &constraints,
759 )
760 }
761
762 #[must_use]
768 pub fn drill_down_filters(
769 &self,
770 row_key: usize,
771 col_key: usize,
772 ) -> Vec<(usize, HashSet<String>)> {
773 let mut out: Vec<(usize, HashSet<String>)> = self
774 .config
775 .filter_fields
776 .iter()
777 .filter_map(|&f| self.filter_values.get(&f).map(|set| (f, set.clone())))
778 .collect();
779 for comp in self
780 .row_path_components(row_key)
781 .into_iter()
782 .chain(self.col_path_components(col_key))
783 {
784 let set = drill_filter_set(&comp.label, &self.config.blank_label);
785 if let Some(entry) = out.iter_mut().find(|(f, _)| *f == comp.field_index) {
786 entry.1 = set;
787 } else {
788 out.push((comp.field_index, set));
789 }
790 }
791 out
792 }
793
794 pub fn request_drill_down(&mut self, row: usize, col: usize) {
799 let (Some(vr), Some(vc)) = (
800 self.visible_rows.get(row).copied(),
801 self.visible_cols.get(col).copied(),
802 ) else {
803 return;
804 };
805 self.pending_drill_down = Some(self.drill_down_filters(vr.key, vc.key));
806 }
807
808 pub(crate) fn take_pending_drill_down(&mut self) -> Option<Vec<(usize, HashSet<String>)>> {
810 self.pending_drill_down.take()
811 }
812
813 fn cell_context(&self, vr: &VisibleRow, vc: &VisibleCol) -> PivotCellContext {
815 let value = self.result.value(vr.key, vc.key).clone();
816 let formatted_value = if matches!(value, CellValue::None) {
817 String::new()
818 } else {
819 format_cell(&value, &self.value_fmt).0
820 };
821 PivotCellContext {
822 row_path: self.row_path_components(vr.key),
823 col_path: self.col_path_components(vc.key),
824 value,
825 formatted_value,
826 is_row_grand_total: vr.kind == VisibleRowKind::GrandTotal,
827 is_col_grand_total: vc.kind == VisibleColKind::GrandTotal,
828 is_row_subtotal: matches!(vr.kind, VisibleRowKind::GroupHeader { .. }),
829 is_col_subtotal: matches!(
830 vc.kind,
831 VisibleColKind::Subtotal | VisibleColKind::Collapsed
832 ),
833 }
834 }
835
836 pub fn open_context_menu(&mut self, hit: PivotHitResult, anchor: Point<Pixels>) {
841 self.filter_popover = None;
842 let resolved: Option<(PivotMenuTarget, usize, usize)> = match hit {
843 PivotHitResult::Cell { row, col } => {
844 match (self.visible_rows.get(row), self.visible_cols.get(col)) {
845 (Some(vr), Some(vc)) => {
846 let ctx = self.cell_context(vr, vc);
847 Some((PivotMenuTarget::Cell(ctx), vr.key, vc.key))
848 }
849 _ => None,
850 }
851 }
852 PivotHitResult::RowHeader { row } | PivotHitResult::RowChevron { row } => {
853 self.visible_rows.get(row).map(|vr| {
854 (
855 PivotMenuTarget::RowHeader {
856 path: self.row_path_components(vr.key),
857 is_grand_total: vr.kind == VisibleRowKind::GrandTotal,
858 },
859 vr.key,
860 TOTAL_KEY,
861 )
862 })
863 }
864 PivotHitResult::ColHeader { level: 0, .. } | PivotHitResult::Corner => {
865 Some((PivotMenuTarget::Corner, TOTAL_KEY, TOTAL_KEY))
866 }
867 PivotHitResult::ColHeader { level, col } => {
868 let depth = level - 1;
869 let key = self
870 .col_ancestor_at(col, depth)
871 .or_else(|| self.visible_cols.get(col).map(|vc| vc.key));
872 key.map(|key| {
873 let is_grand = key == TOTAL_KEY
874 && self
875 .visible_cols
876 .get(col)
877 .is_some_and(|vc| vc.kind == VisibleColKind::GrandTotal);
878 (
879 PivotMenuTarget::ColHeader {
880 path: self.col_path_components(key),
881 is_grand_total: is_grand,
882 },
883 TOTAL_KEY,
884 key,
885 )
886 })
887 }
888 PivotHitResult::ColChevron { col, depth } => {
889 self.col_group_run_at(col, depth).map(|(node, _)| {
890 (
891 PivotMenuTarget::ColHeader {
892 path: self.col_path_components(node),
893 is_grand_total: false,
894 },
895 TOTAL_KEY,
896 node,
897 )
898 })
899 }
900 PivotHitResult::None | PivotHitResult::VScrollbar | PivotHitResult::HScrollbar => None,
901 };
902 let Some((target, row_key, col_key)) = resolved else {
903 self.menu = None;
904 return;
905 };
906
907 let request = PivotContextMenuRequest {
908 source_row_indices: self.source_rows_for(row_key, col_key),
909 target,
910 aggregation: self.config.aggregation,
911 value_field_index: self.config.value_field,
912 value_caption: self.result.value_caption.clone(),
913 config: self.config.clone(),
914 };
915 let items = match &self.context_menu_provider {
916 Some(provider) => provider.menu_items(&request),
917 None => PivotMenuItem::standard_items(&request.target),
918 };
919 if items.is_empty() {
920 self.menu = None;
921 return;
922 }
923 let drill = self.drill_down_filters(row_key, col_key);
924 self.menu = Some(PivotMenu {
925 anchor,
926 items,
927 hovered: None,
928 request,
929 drill,
930 });
931 }
932
933 pub(crate) fn execute_menu_action(&mut self, id: &str, menu: PivotMenu, cx: &mut App) {
936 match id {
937 PIVOT_ACTION_SHOW_SOURCE_ROWS => {
938 self.pending_drill_down = Some(menu.drill);
939 }
940 PIVOT_ACTION_COPY_VALUE => {
941 if let Some(cell) = menu.request.clicked_cell() {
942 cx.write_to_clipboard(gpui::ClipboardItem::new_string(
943 cell.formatted_value.clone(),
944 ));
945 }
946 }
947 PIVOT_ACTION_COPY_CSV => {
948 let csv = self.to_csv();
949 if !csv.is_empty() {
950 cx.write_to_clipboard(gpui::ClipboardItem::new_string(csv));
951 }
952 }
953 _ => {
954 if let Some(provider) = self.context_menu_provider.clone() {
955 provider.on_action(id, &menu.request, self, cx);
956 }
957 }
958 }
959 }
960
961 #[must_use]
967 pub fn row_label(&self, row: &VisibleRow) -> String {
968 match row.kind {
969 VisibleRowKind::GrandTotal => "Grand Total".into(),
970 _ if row.key == TOTAL_KEY => "Total".into(),
971 _ => self
972 .result
973 .row_nodes
974 .get(row.key)
975 .map(|n| n.label.clone())
976 .unwrap_or_default(),
977 }
978 }
979
980 #[must_use]
983 pub fn col_label(&self, col: &VisibleCol) -> String {
984 match col.kind {
985 VisibleColKind::GrandTotal => "Grand Total".into(),
986 VisibleColKind::Subtotal | VisibleColKind::Collapsed => self
987 .result
988 .col_nodes
989 .get(col.key)
990 .map(|n| format!("{} Total", n.label))
991 .unwrap_or_else(|| "Total".into()),
992 _ if col.key == TOTAL_KEY => self.result.value_caption.clone(),
993 _ => self
994 .result
995 .col_nodes
996 .get(col.key)
997 .map(|n| n.label.clone())
998 .unwrap_or_default(),
999 }
1000 }
1001
1002 #[must_use]
1004 pub fn cell_value(&self, row: &VisibleRow, col: &VisibleCol) -> &CellValue {
1005 self.result.value(row.key, col.key)
1006 }
1007
1008 #[must_use]
1015 pub fn header_levels(&self) -> usize {
1016 1 + self.result.col_depth.max(1)
1017 }
1018
1019 #[must_use]
1021 pub fn header_height(&self) -> f32 {
1022 self.header_levels() as f32 * self.header_row_height
1023 }
1024
1025 #[must_use]
1027 pub fn content_size(&self) -> (f32, f32) {
1028 (
1029 self.visible_cols.len() as f32 * self.value_col_width,
1030 self.visible_rows.len() as f32 * self.row_height,
1031 )
1032 }
1033
1034 pub(crate) fn scrollbar_reserved(&self) -> (f32, f32) {
1035 let (cw, ch) = self.content_size();
1036 let vw = f32::from(self.bounds.size.width) - self.row_header_width;
1037 let vh = f32::from(self.bounds.size.height) - self.header_height();
1038 let reserved_w = if ch > vh { SCROLLBAR_SIZE } else { 0.0 };
1039 let reserved_h = if cw > vw { SCROLLBAR_SIZE } else { 0.0 };
1040 (reserved_w, reserved_h)
1041 }
1042
1043 pub(crate) fn max_scroll(&self) -> (f32, f32) {
1044 let (cw, ch) = self.content_size();
1045 let (rw, rh) = self.scrollbar_reserved();
1046 let vw = f32::from(self.bounds.size.width) - self.row_header_width - rw;
1047 let vh = f32::from(self.bounds.size.height) - self.header_height() - rh;
1048 ((cw - vw).max(0.0), (ch - vh).max(0.0))
1049 }
1050
1051 pub(crate) fn clamp_scroll_to_bounds(&mut self) {
1053 let (mx, my) = self.max_scroll();
1054 let s = self.scroll_handle.offset();
1055 let nx = f32::from(s.x).clamp(0.0, mx);
1056 let ny = f32::from(s.y).clamp(0.0, my);
1057 if nx != f32::from(s.x) || ny != f32::from(s.y) {
1058 self.scroll_handle.set_offset(Point {
1059 x: px(nx),
1060 y: px(ny),
1061 });
1062 }
1063 }
1064
1065 fn clamp_selection(&mut self) {
1066 let (nr, nc) = (self.visible_rows.len(), self.visible_cols.len());
1067 if let Some((r1, c1, r2, c2)) = self.selection {
1068 if nr == 0 || nc == 0 || r1 >= nr || c1 >= nc {
1069 self.selection = None;
1070 self.select_anchor = None;
1071 } else {
1072 self.selection = Some((r1, c1, r2.min(nr - 1), c2.min(nc - 1)));
1073 }
1074 }
1075 }
1076
1077 #[must_use]
1079 pub fn hit_test(&self, pos: Point<Pixels>) -> PivotHitResult {
1080 let x = f32::from(pos.x);
1081 let y = f32::from(pos.y);
1082 let bw = f32::from(self.bounds.size.width);
1083 let bh = f32::from(self.bounds.size.height);
1084 if x < 0.0 || y < 0.0 || x > bw || y > bh {
1085 return PivotHitResult::None;
1086 }
1087 let hdr_h = self.header_height();
1088 let (mx, my) = self.max_scroll();
1089 if my > 0.0 && x >= bw - SCROLLBAR_SIZE && y >= hdr_h {
1090 return PivotHitResult::VScrollbar;
1091 }
1092 if mx > 0.0 && y >= bh - SCROLLBAR_SIZE && x >= self.row_header_width {
1093 return PivotHitResult::HScrollbar;
1094 }
1095 let sx = f32::from(self.scroll_handle.offset().x);
1096 let sy = f32::from(self.scroll_handle.offset().y);
1097
1098 if y < hdr_h {
1099 if x < self.row_header_width {
1100 return PivotHitResult::Corner;
1101 }
1102 let level = ((y / self.header_row_height) as usize).min(self.header_levels() - 1);
1103 let cx = x - self.row_header_width + sx;
1104 if cx < 0.0 {
1105 return PivotHitResult::None;
1106 }
1107 let col = (cx / self.value_col_width) as usize;
1108 if col >= self.visible_cols.len() {
1109 return PivotHitResult::None;
1110 }
1111 if level >= 1 {
1114 let depth = level - 1;
1115 if let Some((_, run_start)) = self.col_group_run_at(col, depth) {
1116 let run_x0 = run_start as f32 * self.value_col_width;
1117 let within = cx - run_x0;
1118 let is_group = self
1119 .col_ancestor_at(col, depth)
1120 .map(|n| !self.result.col_nodes[n].is_leaf())
1121 .unwrap_or(false);
1122 if is_group && (2.0..=2.0 + CHEVRON_SIZE).contains(&within) {
1123 return PivotHitResult::ColChevron {
1124 col: run_start,
1125 depth,
1126 };
1127 }
1128 }
1129 }
1130 return PivotHitResult::ColHeader { level, col };
1131 }
1132
1133 if x < self.row_header_width {
1134 let ry = y - hdr_h + sy;
1135 if ry < 0.0 {
1136 return PivotHitResult::None;
1137 }
1138 let row = (ry / self.row_height) as usize;
1139 if row >= self.visible_rows.len() {
1140 return PivotHitResult::None;
1141 }
1142 let vr = self.visible_rows[row];
1143 if matches!(vr.kind, VisibleRowKind::GroupHeader { .. }) {
1144 let indent = vr.depth as f32 * ROW_INDENT + 4.0;
1145 if x >= indent && x <= indent + CHEVRON_SIZE {
1146 return PivotHitResult::RowChevron { row };
1147 }
1148 }
1149 return PivotHitResult::RowHeader { row };
1150 }
1151
1152 let cx = x - self.row_header_width + sx;
1153 let ry = y - hdr_h + sy;
1154 if cx < 0.0 || ry < 0.0 {
1155 return PivotHitResult::None;
1156 }
1157 let col = (cx / self.value_col_width) as usize;
1158 let row = (ry / self.row_height) as usize;
1159 if row < self.visible_rows.len() && col < self.visible_cols.len() {
1160 PivotHitResult::Cell { row, col }
1161 } else {
1162 PivotHitResult::None
1163 }
1164 }
1165
1166 pub(crate) fn col_group_run_at(&self, col: usize, depth: usize) -> Option<(usize, usize)> {
1170 let node = self.col_ancestor_at(col, depth)?;
1171 let mut start = col;
1172 while start > 0 && self.col_ancestor_at(start - 1, depth) == Some(node) {
1173 start -= 1;
1174 }
1175 Some((node, start))
1176 }
1177
1178 pub(crate) fn col_ancestor_at(&self, col: usize, depth: usize) -> Option<usize> {
1180 let vc = self.visible_cols.get(col)?;
1181 if vc.key == TOTAL_KEY {
1182 return None;
1183 }
1184 let mut id = vc.key;
1185 let mut d = self.result.col_nodes.get(id)?.depth;
1186 if depth > d {
1187 return None;
1188 }
1189 while d > depth {
1190 id = self.result.col_nodes[id].parent?;
1191 d -= 1;
1192 }
1193 Some(id)
1194 }
1195
1196 pub fn handle_mouse_down(&mut self, pos: Point<Pixels>, shift: bool) {
1202 let hit = self.hit_test(pos);
1203 match hit {
1204 PivotHitResult::VScrollbar => {
1205 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1206 self.scroll_to_vbar(f32::from(pos.y));
1207 }
1208 PivotHitResult::HScrollbar => {
1209 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1210 self.scroll_to_hbar(f32::from(pos.x));
1211 }
1212 PivotHitResult::RowChevron { row } => {
1213 if let Some(vr) = self.visible_rows.get(row).copied() {
1214 self.toggle_row_group(vr.key);
1215 }
1216 }
1217 PivotHitResult::ColChevron { col, depth } => {
1218 if let Some((node, _)) = self.col_group_run_at(col, depth) {
1219 self.toggle_col_group(node);
1220 } else if let Some(vc) = self.visible_cols.get(col) {
1221 if vc.key != TOTAL_KEY {
1222 self.toggle_col_group(vc.key);
1223 }
1224 }
1225 }
1226 PivotHitResult::Corner => self.cycle_label_sort(),
1227 PivotHitResult::ColHeader { level, col } => {
1228 if level == 0 {
1232 return;
1233 }
1234 if level == self.header_levels() - 1 {
1235 if let Some(vc) = self.visible_cols.get(col).copied() {
1236 self.cycle_sort_by_column(vc.key);
1237 }
1238 } else {
1239 let depth = level - 1;
1240 if let Some((node, _)) = self.col_group_run_at(col, depth) {
1241 if !self.result.col_nodes[node].is_leaf() {
1242 self.toggle_col_group(node);
1243 }
1244 }
1245 }
1246 }
1247 PivotHitResult::RowHeader { row } => {
1248 if self.visible_cols.is_empty() {
1249 return;
1250 }
1251 let last = self.visible_cols.len() - 1;
1252 self.selection = Some((row, 0, row, last));
1253 self.select_anchor = Some((row, 0));
1254 }
1255 PivotHitResult::Cell { row, col } => {
1256 if shift {
1257 let (ar, ac) = self.select_anchor.unwrap_or((row, col));
1258 self.selection = Some(norm_rect(ar, ac, row, col));
1259 } else {
1260 self.selection = Some((row, col, row, col));
1261 self.select_anchor = Some((row, col));
1262 self.is_selecting = true;
1263 }
1264 }
1265 PivotHitResult::None => {
1266 self.selection = None;
1267 self.select_anchor = None;
1268 }
1269 }
1270 }
1271
1272 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, left_down: bool) {
1274 if let Some(axis) = self.scrollbar_drag {
1275 if left_down {
1276 match axis {
1277 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
1278 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
1279 }
1280 return;
1281 }
1282 self.scrollbar_drag = None;
1283 }
1284 let hit = self.hit_test(pos);
1285 self.hover_hit = Some(hit);
1286 if self.is_selecting && left_down {
1287 if let PivotHitResult::Cell { row, col } = hit {
1288 if let Some((ar, ac)) = self.select_anchor {
1289 self.selection = Some(norm_rect(ar, ac, row, col));
1290 }
1291 }
1292 } else if self.is_selecting {
1293 self.is_selecting = false;
1294 }
1295 }
1296
1297 pub fn handle_mouse_up(&mut self) {
1299 self.is_selecting = false;
1300 self.scrollbar_drag = None;
1301 }
1302
1303 pub fn apply_scroll_delta(&mut self, dx: f32, dy: f32) {
1305 let (mx, my) = self.max_scroll();
1306 let s = self.scroll_handle.offset();
1307 let nx = (f32::from(s.x) - dx).clamp(0.0, mx);
1308 let ny = (f32::from(s.y) - dy).clamp(0.0, my);
1309 self.scroll_handle.set_offset(Point {
1310 x: px(nx),
1311 y: px(ny),
1312 });
1313 }
1314
1315 pub(crate) fn scroll_to_vbar(&mut self, mouse_y: f32) {
1316 let (_, my) = self.max_scroll();
1317 let hdr = self.header_height();
1318 let (_, rh) = self.scrollbar_reserved();
1319 let track_h = f32::from(self.bounds.size.height) - hdr - rh;
1320 let (_, ch) = self.content_size();
1321 if track_h <= 0.0 || ch <= 0.0 {
1322 return;
1323 }
1324 let thumb_h = ((track_h * (track_h / ch)).max(20.0)).min(track_h);
1325 let range = (track_h - thumb_h).max(0.0);
1326 let rel = (mouse_y - hdr - thumb_h * 0.5).clamp(0.0, range);
1327 let frac = if range > 0.0 { rel / range } else { 0.0 };
1328 let x = self.scroll_handle.offset().x;
1329 self.scroll_handle.set_offset(Point {
1330 x,
1331 y: px(frac * my),
1332 });
1333 }
1334
1335 pub(crate) fn scroll_to_hbar(&mut self, mouse_x: f32) {
1336 let (mx, _) = self.max_scroll();
1337 let (rw, _) = self.scrollbar_reserved();
1338 let track_x = self.row_header_width;
1339 let track_w = f32::from(self.bounds.size.width) - self.row_header_width - rw;
1340 let (cw, _) = self.content_size();
1341 if track_w <= 0.0 || cw <= 0.0 {
1342 return;
1343 }
1344 let thumb_w = ((track_w * (track_w / cw)).max(20.0)).min(track_w);
1345 let range = (track_w - thumb_w).max(0.0);
1346 let rel = (mouse_x - track_x - thumb_w * 0.5).clamp(0.0, range);
1347 let frac = if range > 0.0 { rel / range } else { 0.0 };
1348 let y = self.scroll_handle.offset().y;
1349 self.scroll_handle.set_offset(Point {
1350 x: px(frac * mx),
1351 y,
1352 });
1353 }
1354
1355 pub fn handle_key(&mut self, ks: &gpui::Keystroke, cx: &mut App) {
1357 if self.key_bindings.copy.matches(ks) {
1358 self.copy_selection(false, cx);
1359 return;
1360 }
1361 if self.key_bindings.copy_with_headers.matches(ks) {
1362 self.copy_selection(true, cx);
1363 return;
1364 }
1365 match ks.key.as_str() {
1366 "escape" => {
1367 self.selection = None;
1368 self.select_anchor = None;
1369 self.filter_popover = None;
1370 self.menu = None;
1371 }
1372 "up" => self.move_selection(0, -1),
1373 "down" => self.move_selection(0, 1),
1374 "left" => self.move_selection(-1, 0),
1375 "right" => self.move_selection(1, 0),
1376 _ => {}
1377 }
1378 }
1379
1380 fn move_selection(&mut self, dx: i32, dy: i32) {
1381 let nr = self.visible_rows.len() as i32;
1382 let nc = self.visible_cols.len() as i32;
1383 if nr == 0 || nc == 0 {
1384 return;
1385 }
1386 let (r, c) = match self.selection {
1387 Some((r1, c1, ..)) => (r1 as i32, c1 as i32),
1388 None => (0, 0),
1389 };
1390 let r = (r + dy).clamp(0, nr - 1) as usize;
1391 let c = (c + dx).clamp(0, nc - 1) as usize;
1392 self.selection = Some((r, c, r, c));
1393 self.select_anchor = Some((r, c));
1394 }
1395
1396 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
1403 let text = self.selection_text(with_headers, '\t');
1404 if !text.is_empty() {
1405 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1406 }
1407 }
1408
1409 #[must_use]
1412 pub fn selection_text(&self, with_headers: bool, sep: char) -> String {
1413 use std::fmt::Write as _;
1414 if self.visible_rows.is_empty() || self.visible_cols.is_empty() {
1415 return String::new();
1416 }
1417 let (r1, c1, r2, c2) = self.selection.unwrap_or((
1418 0,
1419 0,
1420 self.visible_rows.len() - 1,
1421 self.visible_cols.len() - 1,
1422 ));
1423 let mut out = String::new();
1424 if with_headers {
1425 for c in c1..=c2 {
1426 out.push(sep);
1427 out.push_str(&self.col_label(&self.visible_cols[c]));
1428 }
1429 out.push('\n');
1430 }
1431 for r in r1..=r2 {
1432 let vr = &self.visible_rows[r];
1433 if with_headers {
1434 let _ = write!(out, "{}{}", " ".repeat(vr.depth), self.row_label(vr));
1435 out.push(sep);
1436 }
1437 for c in c1..=c2 {
1438 if c > c1 {
1439 out.push(sep);
1440 }
1441 let cell = self.cell_value(vr, &self.visible_cols[c]);
1442 if !matches!(cell, CellValue::None) {
1443 out.push_str(&format_cell(cell, &self.value_fmt).0);
1444 }
1445 }
1446 out.push('\n');
1447 }
1448 out
1449 }
1450
1451 #[must_use]
1455 pub fn to_csv(&self) -> String {
1456 let res = &self.result;
1457 let mut out = String::new();
1458 let col_leaves = res.col_leaves();
1459 let want_grand_col = self.config.show_column_grand_total && !col_leaves.is_empty();
1460
1461 let mut header: Vec<String> = if res.row_depth == 0 {
1462 vec![String::new()]
1463 } else {
1464 res.row_field_names.clone()
1465 };
1466 if col_leaves.is_empty() {
1467 header.push(res.value_caption.clone());
1468 } else {
1469 for &leaf in &col_leaves {
1470 header.push(node_path(&res.col_nodes, leaf).join(" / "));
1471 }
1472 }
1473 if want_grand_col {
1474 header.push("Grand Total".into());
1475 }
1476 push_csv_line(&mut out, &header);
1477
1478 let value_cols: Vec<usize> = if col_leaves.is_empty() {
1479 vec![TOTAL_KEY]
1480 } else {
1481 col_leaves
1482 };
1483 let fmt_value = |cell: &CellValue| {
1484 if matches!(cell, CellValue::None) {
1485 String::new()
1486 } else {
1487 format_cell(cell, &self.value_fmt).0
1488 }
1489 };
1490 let emit_line = |out: &mut String, labels: Vec<String>, row_key: usize| {
1491 let mut fields = labels;
1492 for &ck in &value_cols {
1493 fields.push(fmt_value(res.value(row_key, ck)));
1494 }
1495 if want_grand_col {
1496 fields.push(fmt_value(res.value(row_key, TOTAL_KEY)));
1497 }
1498 push_csv_line(out, &fields);
1499 };
1500
1501 let row_leaves = res.row_leaves();
1502 if row_leaves.is_empty() && res.row_depth == 0 && !res.values.is_empty() {
1503 emit_line(&mut out, vec!["Total".into()], TOTAL_KEY);
1504 }
1505 for &leaf in &row_leaves {
1506 let mut labels = node_path(&res.row_nodes, leaf);
1507 while labels.len() < res.row_depth {
1508 labels.push(String::new());
1509 }
1510 emit_line(&mut out, labels, leaf);
1511 }
1512 if self.config.show_row_grand_total && !row_leaves.is_empty() {
1513 let mut labels = vec!["Grand Total".to_owned()];
1514 while labels.len() < res.row_depth.max(1) {
1515 labels.push(String::new());
1516 }
1517 emit_line(&mut out, labels, TOTAL_KEY);
1518 }
1519 out
1520 }
1521}
1522
1523fn default_value_format() -> ResolvedColumnFormat {
1524 crate::config::GridConfig::default().resolve(usize::MAX, ColumnKind::Decimal)
1525}
1526
1527fn norm_rect(r1: usize, c1: usize, r2: usize, c2: usize) -> (usize, usize, usize, usize) {
1528 (r1.min(r2), c1.min(c2), r1.max(r2), c1.max(c2))
1529}
1530
1531fn push_csv_line(out: &mut String, fields: &[String]) {
1532 for (i, f) in fields.iter().enumerate() {
1533 if i > 0 {
1534 out.push(',');
1535 }
1536 if f.contains(',') || f.contains('"') || f.contains('\n') {
1537 out.push('"');
1538 out.push_str(&f.replace('"', "\"\""));
1539 out.push('"');
1540 } else {
1541 out.push_str(f);
1542 }
1543 }
1544 out.push('\n');
1545}
1546
1547pub(crate) fn node_path(nodes: &[PivotNode], node: usize) -> Vec<String> {
1549 let mut path = Vec::new();
1550 let mut current = Some(node);
1551 while let Some(id) = current {
1552 path.push(nodes[id].label.clone());
1553 current = nodes[id].parent;
1554 }
1555 path.reverse();
1556 path
1557}
1558
1559pub(crate) fn path_components(
1562 nodes: &[PivotNode],
1563 fields: &[usize],
1564 columns: &[Column],
1565 blank_label: &str,
1566 key: usize,
1567) -> Vec<PivotPathComponent> {
1568 if key == TOTAL_KEY || key >= nodes.len() {
1569 return Vec::new();
1570 }
1571 let mut chain = Vec::new();
1572 let mut current = Some(key);
1573 while let Some(id) = current {
1574 chain.push(id);
1575 current = nodes[id].parent;
1576 }
1577 chain.reverse();
1578 chain
1579 .into_iter()
1580 .map(|id| {
1581 let node = &nodes[id];
1582 let field_index = fields.get(node.depth).copied().unwrap_or(usize::MAX);
1583 PivotPathComponent {
1584 field_index,
1585 field_name: columns
1586 .get(field_index)
1587 .map(|c| c.name.clone())
1588 .unwrap_or_default(),
1589 label: node.label.clone(),
1590 group_value: node.sort_key.clone(),
1591 is_blank: node.label == blank_label,
1592 }
1593 })
1594 .collect()
1595}
1596
1597pub(crate) fn rows_matching_path(
1601 rows: &[Vec<CellValue>],
1602 formats: &[ResolvedColumnFormat],
1603 blank_label: &str,
1604 base: &[usize],
1605 constraints: &[(usize, String)],
1606) -> Vec<usize> {
1607 if constraints.is_empty() {
1608 return base.to_vec();
1609 }
1610 base.iter()
1611 .copied()
1612 .filter(|&r| {
1613 constraints.iter().all(|(field, label)| {
1614 let cell = rows
1615 .get(r)
1616 .and_then(|row| row.get(*field))
1617 .unwrap_or(&CellValue::None);
1618 let cell_label = if matches!(cell, CellValue::None) {
1619 blank_label.to_owned()
1620 } else {
1621 format_cell(cell, &formats[*field]).0
1622 };
1623 cell_label == *label
1624 })
1625 })
1626 .collect()
1627}
1628
1629pub(crate) fn drill_filter_set(label: &str, blank_label: &str) -> HashSet<String> {
1634 if label == blank_label {
1635 HashSet::from([String::new(), label.to_owned()])
1636 } else {
1637 HashSet::from([label.to_owned()])
1638 }
1639}
1640
1641fn all_group_paths(nodes: &[PivotNode]) -> HashSet<Vec<String>> {
1643 nodes
1644 .iter()
1645 .enumerate()
1646 .filter(|(_, n)| !n.is_leaf())
1647 .map(|(id, _)| node_path(nodes, id))
1648 .collect()
1649}
1650
1651pub(crate) fn flatten_rows(
1653 result: &PivotResult,
1654 collapsed: &HashSet<Vec<String>>,
1655 config: &PivotConfig,
1656) -> Vec<VisibleRow> {
1657 let mut out = Vec::new();
1658 if result.row_depth == 0 {
1659 if !result.values.is_empty() {
1661 out.push(VisibleRow {
1662 key: TOTAL_KEY,
1663 depth: 0,
1664 kind: VisibleRowKind::Leaf,
1665 zebra: false,
1666 });
1667 }
1668 return out;
1669 }
1670 fn walk(
1671 result: &PivotResult,
1672 collapsed: &HashSet<Vec<String>>,
1673 id: usize,
1674 path: &mut Vec<String>,
1675 zebra: &mut bool,
1676 out: &mut Vec<VisibleRow>,
1677 ) {
1678 let node = &result.row_nodes[id];
1679 path.push(node.label.clone());
1680 if node.is_leaf() {
1681 out.push(VisibleRow {
1682 key: id,
1683 depth: node.depth,
1684 kind: VisibleRowKind::Leaf,
1685 zebra: *zebra,
1686 });
1687 *zebra = !*zebra;
1688 } else if collapsed.contains(path) {
1689 *zebra = false;
1690 out.push(VisibleRow {
1691 key: id,
1692 depth: node.depth,
1693 kind: VisibleRowKind::GroupHeader { expanded: false },
1694 zebra: false,
1695 });
1696 } else {
1697 *zebra = false;
1699 out.push(VisibleRow {
1700 key: id,
1701 depth: node.depth,
1702 kind: VisibleRowKind::GroupHeader { expanded: true },
1703 zebra: false,
1704 });
1705 for &child in &node.children {
1706 walk(result, collapsed, child, path, zebra, out);
1707 }
1708 *zebra = false;
1709 }
1710 path.pop();
1711 }
1712 let mut path = Vec::new();
1713 let mut zebra = false;
1714 for &root in &result.row_roots {
1715 walk(result, collapsed, root, &mut path, &mut zebra, &mut out);
1716 }
1717 if config.show_row_grand_total && !out.is_empty() {
1718 out.push(VisibleRow {
1719 key: TOTAL_KEY,
1720 depth: 0,
1721 kind: VisibleRowKind::GrandTotal,
1722 zebra: false,
1723 });
1724 }
1725 out
1726}
1727
1728pub(crate) fn flatten_cols(
1730 result: &PivotResult,
1731 collapsed: &HashSet<Vec<String>>,
1732 config: &PivotConfig,
1733) -> Vec<VisibleCol> {
1734 let mut out = Vec::new();
1735 if result.col_depth == 0 {
1736 if !result.values.is_empty() {
1737 out.push(VisibleCol {
1738 key: TOTAL_KEY,
1739 depth: 0,
1740 kind: VisibleColKind::Leaf,
1741 });
1742 }
1743 return out;
1744 }
1745 fn walk(
1746 result: &PivotResult,
1747 collapsed: &HashSet<Vec<String>>,
1748 config: &PivotConfig,
1749 id: usize,
1750 path: &mut Vec<String>,
1751 out: &mut Vec<VisibleCol>,
1752 ) {
1753 let node = &result.col_nodes[id];
1754 path.push(node.label.clone());
1755 if node.is_leaf() {
1756 out.push(VisibleCol {
1757 key: id,
1758 depth: node.depth,
1759 kind: VisibleColKind::Leaf,
1760 });
1761 } else if collapsed.contains(path) {
1762 out.push(VisibleCol {
1763 key: id,
1764 depth: node.depth,
1765 kind: VisibleColKind::Collapsed,
1766 });
1767 } else {
1768 for &child in &node.children {
1769 walk(result, collapsed, config, child, path, out);
1770 }
1771 if config.show_column_subtotals {
1772 out.push(VisibleCol {
1773 key: id,
1774 depth: node.depth,
1775 kind: VisibleColKind::Subtotal,
1776 });
1777 }
1778 }
1779 path.pop();
1780 }
1781 let mut path = Vec::new();
1782 for &root in &result.col_roots {
1783 walk(result, collapsed, config, root, &mut path, &mut out);
1784 }
1785 if config.show_column_grand_total && !out.is_empty() {
1786 out.push(VisibleCol {
1787 key: TOTAL_KEY,
1788 depth: 0,
1789 kind: VisibleColKind::GrandTotal,
1790 });
1791 }
1792 out
1793}
1794
1795fn sort_axis_by_key(nodes: &mut [PivotNode], roots: &mut [usize], descending: bool) {
1797 let keys: Vec<CellValue> = nodes.iter().map(|n| n.sort_key.clone()).collect();
1798 sort_axis_by(nodes, roots, &keys, descending);
1799}
1800
1801fn sort_axis_by(
1803 nodes: &mut [PivotNode],
1804 roots: &mut [usize],
1805 keys: &[CellValue],
1806 descending: bool,
1807) {
1808 let cmp = |a: &usize, b: &usize| {
1809 let ord = compare_cells(&keys[*a], &keys[*b]);
1810 if descending {
1811 ord.reverse()
1812 } else {
1813 ord
1814 }
1815 };
1816 roots.sort_by(cmp);
1817 for node in nodes.iter_mut() {
1818 let mut children = std::mem::take(&mut node.children);
1819 children.sort_by(cmp);
1820 node.children = children;
1821 }
1822}
1823
1824#[cfg(test)]
1825#[allow(clippy::unwrap_used)]
1826mod tests {
1827 use super::*;
1828 use crate::config::GridConfig;
1829 use crate::data::ColumnKind;
1830 use CellValue::{Decimal, Integer, Text};
1831
1832 fn fixture_result(config: &PivotConfig) -> PivotResult {
1833 let columns = vec![
1834 Column::new("region", ColumnKind::Text, 100.0),
1835 Column::new("product", ColumnKind::Text, 100.0),
1836 Column::new("year", ColumnKind::Integer, 80.0),
1837 Column::new("amount", ColumnKind::Decimal, 100.0),
1838 ];
1839 let r = |region: &str, product: &str, year: i64, amount: f64| {
1840 vec![
1841 Text(region.into()),
1842 Text(product.into()),
1843 Integer(year),
1844 Decimal(amount),
1845 ]
1846 };
1847 let rows = vec![
1848 r("Europe", "Widget", 2023, 10.0),
1849 r("Europe", "Widget", 2024, 20.0),
1850 r("Europe", "Gadget", 2023, 5.0),
1851 r("Asia", "Widget", 2023, 7.0),
1852 r("Asia", "Gadget", 2024, 3.0),
1853 ];
1854 let formats = GridConfig::default().resolve_all(&columns);
1855 let all: Vec<usize> = (0..rows.len()).collect();
1856 compute_pivot(&columns, &rows, &all, config, &formats)
1857 }
1858
1859 fn two_level_config() -> PivotConfig {
1860 PivotConfig {
1861 row_fields: vec![0, 1],
1862 column_fields: vec![2],
1863 value_field: Some(3),
1864 ..PivotConfig::default()
1865 }
1866 }
1867
1868 #[test]
1869 fn flatten_rows_expanded_lists_headers_and_leaves() {
1870 let cfg = two_level_config();
1871 let result = fixture_result(&cfg);
1872 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
1873 assert_eq!(rows.len(), 7);
1875 assert_eq!(rows[0].kind, VisibleRowKind::GroupHeader { expanded: true });
1876 assert_eq!(rows[0].depth, 0);
1877 assert_eq!(rows[1].kind, VisibleRowKind::Leaf);
1878 assert_eq!(rows[1].depth, 1);
1879 assert_eq!(rows[6].kind, VisibleRowKind::GrandTotal);
1880 }
1881
1882 #[test]
1883 fn flatten_rows_collapse_hides_children() {
1884 let cfg = two_level_config();
1885 let result = fixture_result(&cfg);
1886 let mut collapsed = HashSet::new();
1887 collapsed.insert(vec!["Asia".to_owned()]);
1888 let rows = flatten_rows(&result, &collapsed, &cfg);
1889 assert_eq!(rows.len(), 5);
1891 assert_eq!(
1892 rows[0].kind,
1893 VisibleRowKind::GroupHeader { expanded: false }
1894 );
1895 assert_eq!(rows[1].kind, VisibleRowKind::GroupHeader { expanded: true });
1896 }
1897
1898 #[test]
1899 fn flatten_rows_without_grand_total() {
1900 let mut cfg = two_level_config();
1901 cfg.show_row_grand_total = false;
1902 let result = fixture_result(&cfg);
1903 let rows = flatten_rows(&result, &HashSet::new(), &cfg);
1904 assert!(rows.iter().all(|r| r.kind != VisibleRowKind::GrandTotal));
1905 }
1906
1907 #[test]
1908 fn flatten_cols_leaves_plus_grand_total() {
1909 let cfg = two_level_config();
1910 let result = fixture_result(&cfg);
1911 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
1912 assert_eq!(cols.len(), 3);
1914 assert_eq!(cols[0].kind, VisibleColKind::Leaf);
1915 assert_eq!(cols[2].kind, VisibleColKind::GrandTotal);
1916 }
1917
1918 #[test]
1919 fn flatten_cols_collapsed_group_is_single_column() {
1920 let mut cfg = two_level_config();
1921 cfg.row_fields = vec![0];
1923 cfg.column_fields = vec![2, 1];
1924 let result = fixture_result(&cfg);
1925 let first_year = result.col_roots[0];
1928 let mut collapsed = HashSet::new();
1929 collapsed.insert(vec![result.col_nodes[first_year].label.clone()]);
1930 let cols = flatten_cols(&result, &collapsed, &cfg);
1931 assert_eq!(cols[0].kind, VisibleColKind::Collapsed);
1933 assert!(cols.len() >= 3);
1934 }
1935
1936 #[test]
1937 fn flatten_cols_subtotal_columns_follow_expanded_groups() {
1938 let mut cfg = two_level_config();
1939 cfg.row_fields = vec![0];
1940 cfg.column_fields = vec![2, 1];
1941 cfg.show_column_subtotals = true;
1942 let result = fixture_result(&cfg);
1943 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
1944 let subtotal_count = cols
1945 .iter()
1946 .filter(|c| c.kind == VisibleColKind::Subtotal)
1947 .count();
1948 assert_eq!(subtotal_count, 2); let first_sub = cols
1951 .iter()
1952 .position(|c| c.kind == VisibleColKind::Subtotal)
1953 .unwrap();
1954 assert!(first_sub > 0);
1955 assert_eq!(cols[first_sub - 1].kind, VisibleColKind::Leaf);
1956 }
1957
1958 #[test]
1959 fn flatten_cols_no_column_fields_yields_single_value_column() {
1960 let mut cfg = two_level_config();
1961 cfg.column_fields = vec![];
1962 let result = fixture_result(&cfg);
1963 let cols = flatten_cols(&result, &HashSet::new(), &cfg);
1964 assert_eq!(cols.len(), 1);
1965 assert_eq!(cols[0].key, TOTAL_KEY);
1966 }
1967
1968 #[test]
1969 fn flatten_empty_result_is_empty() {
1970 let cfg = PivotConfig::default();
1971 let result = PivotResult::default();
1972 assert!(flatten_rows(&result, &HashSet::new(), &cfg).is_empty());
1973 assert!(flatten_cols(&result, &HashSet::new(), &cfg).is_empty());
1974 }
1975
1976 #[test]
1977 fn sort_axis_descending_reverses_roots_and_children() {
1978 let cfg = two_level_config();
1979 let mut result = fixture_result(&cfg);
1980 let labels = |result: &PivotResult| -> Vec<String> {
1981 result
1982 .row_roots
1983 .iter()
1984 .map(|&r| result.row_nodes[r].label.clone())
1985 .collect()
1986 };
1987 assert_eq!(labels(&result), vec!["Asia", "Europe"]);
1988 let mut roots = result.row_roots.clone();
1989 sort_axis_by_key(&mut result.row_nodes, &mut roots, true);
1990 result.row_roots = roots;
1991 assert_eq!(labels(&result), vec!["Europe", "Asia"]);
1992 let asia = result.row_roots[1];
1993 let child_labels: Vec<&str> = result.row_nodes[asia]
1994 .children
1995 .iter()
1996 .map(|&c| result.row_nodes[c].label.as_str())
1997 .collect();
1998 assert_eq!(child_labels, vec!["Widget", "Gadget"]);
1999 }
2000
2001 #[test]
2002 fn sort_axis_by_values_orders_missing_first_ascending() {
2003 let cfg = two_level_config();
2004 let mut result = fixture_result(&cfg);
2005 let y2024 = result
2007 .col_roots
2008 .iter()
2009 .copied()
2010 .find(|&c| result.col_nodes[c].sort_key == Integer(2024))
2011 .unwrap();
2012 let keys: Vec<CellValue> = (0..result.row_nodes.len())
2013 .map(|id| {
2014 result
2015 .values
2016 .get(&(id, y2024))
2017 .cloned()
2018 .unwrap_or(CellValue::None)
2019 })
2020 .collect();
2021 let mut roots = result.row_roots.clone();
2022 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, false);
2023 let labels: Vec<&str> = roots
2024 .iter()
2025 .map(|&r| result.row_nodes[r].label.as_str())
2026 .collect();
2027 assert_eq!(labels, vec!["Asia", "Europe"]);
2028 sort_axis_by(&mut result.row_nodes, &mut roots, &keys, true);
2029 let labels: Vec<&str> = roots
2030 .iter()
2031 .map(|&r| result.row_nodes[r].label.as_str())
2032 .collect();
2033 assert_eq!(labels, vec!["Europe", "Asia"]);
2034 }
2035
2036 #[test]
2037 fn node_path_walks_to_root() {
2038 let cfg = two_level_config();
2039 let result = fixture_result(&cfg);
2040 let europe = result
2041 .row_nodes
2042 .iter()
2043 .position(|n| n.label == "Europe")
2044 .unwrap();
2045 let widget = result.row_nodes[europe]
2046 .children
2047 .iter()
2048 .copied()
2049 .find(|&c| result.row_nodes[c].label == "Widget")
2050 .unwrap();
2051 assert_eq!(
2052 node_path(&result.row_nodes, widget),
2053 vec!["Europe".to_owned(), "Widget".to_owned()]
2054 );
2055 }
2056
2057 #[test]
2058 fn all_group_paths_lists_only_non_leaves() {
2059 let cfg = two_level_config();
2060 let result = fixture_result(&cfg);
2061 let paths = all_group_paths(&result.row_nodes);
2062 assert_eq!(paths.len(), 2); assert!(paths.contains(&vec!["Asia".to_owned()]));
2064 }
2065
2066 #[test]
2067 fn csv_line_quotes_fields_with_commas_and_quotes() {
2068 let mut out = String::new();
2069 push_csv_line(
2070 &mut out,
2071 &["a,b".to_owned(), "plain".to_owned(), "q\"q".to_owned()],
2072 );
2073 assert_eq!(out, "\"a,b\",plain,\"q\"\"q\"\n");
2074 }
2075
2076 fn fixture_columns() -> Vec<Column> {
2077 vec![
2078 Column::new("region", ColumnKind::Text, 100.0),
2079 Column::new("product", ColumnKind::Text, 100.0),
2080 Column::new("year", ColumnKind::Integer, 80.0),
2081 Column::new("amount", ColumnKind::Decimal, 100.0),
2082 ]
2083 }
2084
2085 fn fixture_rows() -> Vec<Vec<CellValue>> {
2086 let r = |region: &str, product: &str, year: i64, amount: f64| {
2087 vec![
2088 Text(region.into()),
2089 Text(product.into()),
2090 Integer(year),
2091 Decimal(amount),
2092 ]
2093 };
2094 vec![
2095 r("Europe", "Widget", 2023, 10.0),
2096 r("Europe", "Widget", 2024, 20.0),
2097 r("Europe", "Gadget", 2023, 5.0),
2098 r("Asia", "Widget", 2023, 7.0),
2099 r("Asia", "Gadget", 2024, 3.0),
2100 ]
2101 }
2102
2103 #[test]
2104 fn path_components_walk_root_to_leaf_with_field_metadata() {
2105 let cfg = two_level_config();
2106 let result = fixture_result(&cfg);
2107 let columns = fixture_columns();
2108 let europe = result
2109 .row_nodes
2110 .iter()
2111 .position(|n| n.label == "Europe")
2112 .unwrap();
2113 let widget = result.row_nodes[europe]
2114 .children
2115 .iter()
2116 .copied()
2117 .find(|&c| result.row_nodes[c].label == "Widget")
2118 .unwrap();
2119 let path = path_components(
2120 &result.row_nodes,
2121 &cfg.row_fields,
2122 &columns,
2123 "(blank)",
2124 widget,
2125 );
2126 assert_eq!(path.len(), 2);
2127 assert_eq!(path[0].field_name, "region");
2128 assert_eq!(path[0].label, "Europe");
2129 assert_eq!(path[0].field_index, 0);
2130 assert_eq!(path[1].field_name, "product");
2131 assert_eq!(path[1].label, "Widget");
2132 assert!(!path[1].is_blank);
2133 assert!(path_components(
2135 &result.row_nodes,
2136 &cfg.row_fields,
2137 &columns,
2138 "(blank)",
2139 TOTAL_KEY
2140 )
2141 .is_empty());
2142 }
2143
2144 #[test]
2145 fn rows_matching_path_selects_only_driving_rows() {
2146 let rows = fixture_rows();
2147 let columns = fixture_columns();
2148 let formats = GridConfig::default().resolve_all(&columns);
2149 let base: Vec<usize> = (0..rows.len()).collect();
2150 let constraints = vec![(0, "Europe".to_owned()), (1, "Widget".to_owned())];
2152 assert_eq!(
2153 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2154 vec![0, 1]
2155 );
2156 assert_eq!(
2158 rows_matching_path(&rows, &formats, "(blank)", &base, &[]),
2159 base
2160 );
2161 assert_eq!(
2163 rows_matching_path(&rows, &formats, "(blank)", &[1, 2, 3], &constraints),
2164 vec![1]
2165 );
2166 }
2167
2168 #[test]
2169 fn rows_matching_path_buckets_nulls_under_blank_label() {
2170 let mut rows = fixture_rows();
2171 rows.push(vec![
2172 CellValue::None,
2173 Text("Widget".into()),
2174 Integer(2023),
2175 Decimal(2.0),
2176 ]);
2177 let columns = fixture_columns();
2178 let formats = GridConfig::default().resolve_all(&columns);
2179 let base: Vec<usize> = (0..rows.len()).collect();
2180 let constraints = vec![(0, "(blank)".to_owned())];
2181 assert_eq!(
2182 rows_matching_path(&rows, &formats, "(blank)", &base, &constraints),
2183 vec![5]
2184 );
2185 }
2186
2187 #[test]
2188 fn drill_filter_set_maps_blank_to_empty_formatted_value() {
2189 let set = drill_filter_set("(blank)", "(blank)");
2190 assert!(set.contains(""));
2191 assert!(set.contains("(blank)"));
2192 let set = drill_filter_set("Europe", "(blank)");
2193 assert_eq!(set, HashSet::from(["Europe".to_owned()]));
2194 }
2195}