1use crate::compare_cells;
6use crate::data::{CellValue, ColumnKind, GridData, GridDataError};
7use crate::filter::{
8 cell_passes_filter, parse_ymd_to_unix, uses_number_ops, ColumnFilter, FilterPredicate,
9 NumberOp, TextOp,
10};
11use crate::format::format_cell;
12use crate::grid::state::state_inner::apply_edge_scroll;
13use crate::grid::theme::{GridTheme, GridThemePair};
14
15use crate::config::{GridConfig, ResolvedColumnFormat};
16use gpui::{
17 px, App, Bounds, FocusHandle, Keystroke, MouseButton, Pixels, Point, ScrollHandle, Size,
18};
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21
22use crate::grid::menu as menu_mod;
24#[allow(unused_imports)]
25pub(crate) use crate::grid::menu::{ContextMenu, MenuAction, MenuItem};
26use crate::grid::selection::{
27 is_cell_selected, is_row_selected, HitResult, ScrollbarAxis, Selection, SortDirection,
28};
29
30use crate::grid::context_menu::{
31 ColumnContext, ContextMenuItem, ContextMenuProviderHandle, ContextMenuRequest,
32 ContextMenuSelection, ContextMenuTarget, PendingCustomContextMenuAction,
33};
34
35pub mod state_inner {
39 use super::{
40 format_cell, CellValue, GridState, HitResult, Pixels, Point, ResolvedColumnFormat,
41 };
42 pub use crate::grid::selection::screen_to_content;
43 pub use crate::grid::selection::to_grid_relative;
44 use std::fmt::Write as _;
45
46 const REALLY_FAST: f32 = 16.0;
60 pub fn edge_scroll_speed(dist_from_edge: f32) -> f32 {
61 if dist_from_edge > 90.0 {
62 return 0.0;
63 }
64 if dist_from_edge < 0.0 {
65 return REALLY_FAST;
68 }
69 if dist_from_edge < 30.0 {
70 REALLY_FAST
71 } else if dist_from_edge < 60.0 {
72 8.0
73 } else {
74 4.0
75 }
76 }
77
78 pub fn apply_edge_scroll(state: &mut GridState) -> bool {
79 if !state.is_dragging {
80 return false;
81 }
82 let Some(pos) = state.last_mouse_pos else {
83 return false;
84 };
85 let bounds = state.bounds;
86 let vw: f32 = bounds.size.width.into();
95 let vh: f32 = bounds.size.height.into();
96 let px: f32 = pos.x.into();
97 let py: f32 = pos.y.into();
98 let right_dist = vw - px;
99 let left_dist = px - state.row_header_width;
100 let bottom_dist = vh - py;
101 let top_dist = py - state.header_height;
102 let mut dx = 0.0_f32;
103 let mut dy = 0.0_f32;
104 if right_dist < 90.0 && right_dist <= left_dist {
105 dx = edge_scroll_speed(right_dist);
106 } else if left_dist < 90.0 {
107 dx = -edge_scroll_speed(left_dist);
108 }
109 if bottom_dist < 90.0 && bottom_dist <= top_dist {
110 dy = edge_scroll_speed(bottom_dist);
111 } else if top_dist < 90.0 {
112 dy = -edge_scroll_speed(top_dist);
113 }
114 if dx == 0.0 && dy == 0.0 {
115 return false;
116 }
117 state.scroll_one_edge_tick(dx, dy);
118 if state.drag_start.is_some() {
119 state.update_drag_from_last();
120 }
121 true
122 }
123
124 #[must_use]
125 pub fn format_current_status(state: &GridState) -> String {
126 let scroll = state.scroll_handle.offset();
127 let (click_col, click_row) = col_row_from_hit(state.click_hit);
128 let (hover_col, hover_row) = col_row_from_hit(state.hover_hit);
129 let mut out = String::new();
130 let _ = write!(
131 out,
132 "Click: {} Scroll@Click: {} Cell: {} | Cur: {} Scroll: {} Over: {}",
133 fmt_point(state.click_pos),
134 fmt_point(state.scroll_at_click),
135 fmt_cr(click_col, click_row),
136 fmt_point(state.last_mouse_pos),
137 fmt_point(Some(scroll)),
138 fmt_cr(hover_col, hover_row),
139 );
140 out
141 }
142
143 fn col_row_from_hit(hit: Option<HitResult>) -> (Option<usize>, Option<usize>) {
144 match hit {
145 Some(HitResult::Cell(r, c)) => (Some(c), Some(r)),
146 Some(HitResult::RowHeader(r)) => (None, Some(r)),
147 Some(HitResult::ColumnHeader(c)) | Some(HitResult::SortButton(c)) => (Some(c), None),
148 _ => (None, None),
149 }
150 }
151
152 fn fmt_point(p: Option<Point<Pixels>>) -> String {
153 match p {
154 Some(p) => format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y)),
155 None => "—".into(),
156 }
157 }
158
159 fn fmt_cr(c: Option<usize>, r: Option<usize>) -> String {
160 match (c, r) {
161 (Some(c), Some(r)) => format!("(col {c}, row {r})"),
162 (Some(c), None) => format!("(col {c})"),
163 (None, Some(r)) => format!("(row {r})"),
164 (None, None) => "—".into(),
165 }
166 }
167
168 #[must_use]
169 pub fn cell_text(cell: &CellValue, fmt: &ResolvedColumnFormat) -> String {
170 format_cell(cell, fmt).0
171 }
172}
173
174pub const SCROLLBAR_SIZE: f32 = 20.0;
176pub const EDGE_SCROLL_TICK_MS: u64 = 16;
178
179#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct RowGroup {
182 pub label: String,
184 pub row_count: usize,
187 pub collapsed: bool,
189}
190
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
193pub(crate) enum GridDisplayRow {
194 GroupHeader { group: usize },
195 Data { source_row: usize, flat_row: usize },
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct RowWindow {
207 pub total_rows: usize,
209 pub offset: usize,
211}
212
213#[derive(Debug)]
215pub struct GridState {
216 pub data: GridData,
217 pub config: GridConfig,
218 pub window: Option<RowWindow>,
220 pub resolved_formats: Arc<Vec<ResolvedColumnFormat>>,
228 pub(crate) data_rows: Arc<Vec<Vec<CellValue>>>,
232 pub display_indices: Arc<Vec<usize>>,
233 pub(crate) display_rows: Arc<Vec<GridDisplayRow>>,
234 grouped_column: Option<usize>,
235 pub(crate) row_groups: Arc<Vec<RowGroup>>,
236 collapsed_group_labels: HashSet<String>,
237 pub selection: Selection,
238 pub(crate) range_anchor: Option<(usize, usize)>,
242 pub(crate) range_active: Option<(usize, usize)>,
245 pub sort: Option<(usize, SortDirection)>,
246 pub filters: Vec<ColumnFilter>,
247 pub scroll_handle: ScrollHandle,
248 pub focus_handle: FocusHandle,
249 pub bounds: Bounds<Pixels>,
250 pub row_height: f32,
251 pub header_height: f32,
252 pub row_header_width: f32,
253 pub font_size: f32,
254 pub char_width: f32,
255 pub theme: GridTheme,
256 pub theme_family: GridThemePair,
262 pub is_dragging: bool,
263 pub drag_start: Option<Point<Pixels>>,
264 pub drag_start_hit: Option<HitResult>,
265 pub scroll_at_click: Option<Point<Pixels>>,
266 pub last_mouse_pos: Option<Point<Pixels>>,
267 pub status_bar_height: f32,
268 pub debug_bar_enabled: bool,
273 pub click_pos: Option<Point<Pixels>>,
274 pub click_hit: Option<HitResult>,
275 pub hover_hit: Option<HitResult>,
276 pub resizing_col: Option<usize>,
277 pub resize_start_x: f32,
278 pub resize_start_width: f32,
279 pub context_menu: Option<ContextMenu>,
280 pub filter_panel: Option<FilterPanel>,
281 pub pending_action: Option<(MenuAction, usize)>,
282 pub(crate) pending_custom_context_menu_action: Option<PendingCustomContextMenuAction>,
283 pub(crate) context_menu_provider: Option<ContextMenuProviderHandle>,
284 pub scrollbar_drag: Option<ScrollbarAxis>,
285 pub scrollbar_drag_start_offset: f32,
286 pub scrollbar_drag_start_pos: f32,
287 pub(crate) window_viewport: Size<Pixels>,
291 pub(crate) edge_scroll_active: bool,
295 pub(crate) column_meta: Arc<[ColumnContext]>,
299 pub(crate) self_weak: Option<gpui::WeakEntity<GridState>>,
303 pub(crate) busy: Option<BusyState>,
307}
308
309#[derive(Clone, Debug)]
313pub struct BusyState {
314 pub label: String,
316 pub progress: Option<f32>,
319}
320
321#[derive(Clone, Debug, Default)]
325pub struct TextInput {
326 pub value: String,
328 pub cursor_chars: usize,
330}
331
332impl TextInput {
333 fn new(value: String) -> Self {
334 let cursor_chars = value.chars().count();
335 Self {
336 value,
337 cursor_chars,
338 }
339 }
340
341 fn clamp_cursor(&mut self) {
342 let total = self.value.chars().count();
343 if self.cursor_chars > total {
344 self.cursor_chars = total;
345 }
346 }
347
348 fn insert_char(&mut self, ch: char) {
349 let byte_idx = byte_index_for_char(&self.value, self.cursor_chars);
350 self.value.insert(byte_idx, ch);
351 self.cursor_chars += 1;
352 }
353
354 fn backspace(&mut self) {
355 if self.cursor_chars == 0 {
356 return;
357 }
358 let end = byte_index_for_char(&self.value, self.cursor_chars);
359 let start = byte_index_for_char(&self.value, self.cursor_chars - 1);
360 self.value.replace_range(start..end, "");
361 self.cursor_chars -= 1;
362 }
363
364 fn move_left(&mut self) {
365 if self.cursor_chars > 0 {
366 self.cursor_chars -= 1;
367 }
368 }
369
370 fn move_right(&mut self) {
371 self.clamp_cursor();
372 if self.cursor_chars < self.value.chars().count() {
373 self.cursor_chars += 1;
374 }
375 }
376}
377
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
380pub enum FilterInput {
381 Search,
383 OperandA,
385 OperandB,
387}
388
389#[derive(Clone, Debug)]
391pub struct FilterValueRow {
392 pub label: String,
394 pub checked: bool,
396}
397
398#[derive(Clone, Debug)]
405pub struct FilterPanel {
406 pub col: usize,
408 pub anchor: Point<Pixels>,
410 pub kind: ColumnKind,
412 pub search: TextInput,
414 pub op_index: usize,
417 pub op_menu_open: bool,
419 pub operand_a: TextInput,
421 pub operand_b: TextInput,
423 pub focus: FilterInput,
425 pub auto_apply: bool,
427 pub distinct: Vec<FilterValueRow>,
429}
430
431const TEXT_OP_LABELS: &[&str] = &[
435 "Choose One",
436 "contains",
437 "does not contain",
438 "begins with",
439 "ends with",
440 "is",
441 "is not",
442 "matches (regex)",
443];
444
445const NUMBER_OP_LABELS: &[&str] = &[
447 "Choose One",
448 "equal to",
449 "not equal to",
450 "greater than",
451 "greater than or equal to",
452 "less than",
453 "less than or equal to",
454 "between",
455 "not between",
456];
457
458impl FilterPanel {
459 #[must_use]
461 pub fn op_labels(&self) -> &'static [&'static str] {
462 if uses_number_ops(self.kind) {
463 NUMBER_OP_LABELS
464 } else {
465 TEXT_OP_LABELS
466 }
467 }
468
469 #[must_use]
471 pub fn current_op_label(&self) -> &'static str {
472 self.op_labels()
473 .get(self.op_index)
474 .copied()
475 .unwrap_or("Choose One")
476 }
477
478 #[must_use]
480 pub fn needs_operand(&self) -> bool {
481 self.op_index != 0
482 }
483
484 #[must_use]
486 pub fn needs_second_operand(&self) -> bool {
487 uses_number_ops(self.kind) && matches!(self.op_index, 7 | 8)
488 }
489
490 fn text_op_for_index(index: usize) -> Option<TextOp> {
491 match index {
492 1 => Some(TextOp::Contains),
493 2 => Some(TextOp::DoesNotContain),
494 3 => Some(TextOp::BeginsWith),
495 4 => Some(TextOp::EndsWith),
496 5 => Some(TextOp::Is),
497 6 => Some(TextOp::IsNot),
498 7 => Some(TextOp::Matches),
499 _ => None,
500 }
501 }
502
503 fn number_op_for_index(index: usize) -> Option<NumberOp> {
504 match index {
505 1 => Some(NumberOp::Eq),
506 2 => Some(NumberOp::Ne),
507 3 => Some(NumberOp::Gt),
508 4 => Some(NumberOp::Ge),
509 5 => Some(NumberOp::Lt),
510 6 => Some(NumberOp::Le),
511 7 => Some(NumberOp::Between),
512 8 => Some(NumberOp::NotBetween),
513 _ => None,
514 }
515 }
516
517 fn active_input_mut(&mut self) -> &mut TextInput {
518 match self.focus {
519 FilterInput::Search => &mut self.search,
520 FilterInput::OperandA => &mut self.operand_a,
521 FilterInput::OperandB => &mut self.operand_b,
522 }
523 }
524
525 #[must_use]
529 pub fn visible_indices(&self) -> Vec<usize> {
530 let needle = self.search.value.to_lowercase();
531 self.distinct
532 .iter()
533 .enumerate()
534 .filter(|(_, row)| needle.is_empty() || row.label.to_lowercase().contains(&needle))
535 .map(|(i, _)| i)
536 .collect()
537 }
538
539 #[must_use]
543 pub fn all_checked(&self) -> bool {
544 !self.distinct.is_empty() && self.distinct.iter().all(|r| r.checked)
545 }
546
547 fn to_filter(&self) -> ColumnFilter {
550 let predicate = self.build_predicate();
551 let all_checked = self.distinct.iter().all(|r| r.checked);
552 let values = if all_checked {
553 None
554 } else {
555 Some(
556 self.distinct
557 .iter()
558 .filter(|r| r.checked)
559 .map(|r| r.label.clone())
560 .collect(),
561 )
562 };
563 ColumnFilter { predicate, values }
564 }
565
566 fn build_predicate(&self) -> FilterPredicate {
567 if self.op_index == 0 {
568 return FilterPredicate::None;
569 }
570 if uses_number_ops(self.kind) {
571 let Some(op) = Self::number_op_for_index(self.op_index) else {
572 return FilterPredicate::None;
573 };
574 let Some(a) = self.parse_number_operand(&self.operand_a.value) else {
575 return FilterPredicate::None;
576 };
577 let b = if self.needs_second_operand() {
578 self.parse_number_operand(&self.operand_b.value)
579 .unwrap_or(a)
580 } else {
581 a
582 };
583 FilterPredicate::Number { op, a, b }
584 } else {
585 let Some(op) = Self::text_op_for_index(self.op_index) else {
586 return FilterPredicate::None;
587 };
588 FilterPredicate::Text {
589 op,
590 operand: self.operand_a.value.clone(),
591 }
592 }
593 }
594
595 fn parse_number_operand(&self, s: &str) -> Option<f64> {
596 let t = s.trim();
597 if t.is_empty() {
598 return None;
599 }
600 if self.kind == ColumnKind::Date {
601 return parse_ymd_to_unix(t).map(|v| v as f64);
602 }
603 t.replace(',', "").parse::<f64>().ok()
605 }
606}
607
608fn byte_index_for_char(input: &str, char_idx: usize) -> usize {
609 input
610 .char_indices()
611 .nth(char_idx)
612 .map_or(input.len(), |(idx, _)| idx)
613}
614
615fn seed_operator(kind: ColumnKind, predicate: &FilterPredicate) -> (usize, String, String) {
618 match predicate {
619 FilterPredicate::None => (0, String::new(), String::new()),
620 FilterPredicate::Text { op, operand } => {
621 (text_op_index(*op), operand.clone(), String::new())
622 }
623 FilterPredicate::Number { op, a, b } => {
624 let b_str = if matches!(op, NumberOp::Between | NumberOp::NotBetween) {
625 fmt_number_operand(kind, *b)
626 } else {
627 String::new()
628 };
629 (number_op_index(*op), fmt_number_operand(kind, *a), b_str)
630 }
631 }
632}
633
634fn text_op_index(op: TextOp) -> usize {
635 match op {
636 TextOp::Contains => 1,
637 TextOp::DoesNotContain => 2,
638 TextOp::BeginsWith => 3,
639 TextOp::EndsWith => 4,
640 TextOp::Is => 5,
641 TextOp::IsNot => 6,
642 TextOp::Matches => 7,
643 }
644}
645
646fn number_op_index(op: NumberOp) -> usize {
647 match op {
648 NumberOp::Eq => 1,
649 NumberOp::Ne => 2,
650 NumberOp::Gt => 3,
651 NumberOp::Ge => 4,
652 NumberOp::Lt => 5,
653 NumberOp::Le => 6,
654 NumberOp::Between => 7,
655 NumberOp::NotBetween => 8,
656 }
657}
658
659fn fmt_number_operand(kind: ColumnKind, v: f64) -> String {
660 if kind == ColumnKind::Date {
661 let secs = v as i64;
662 let fmt = crate::config::DateFormat {
663 format: "%Y-%m-%d".into(),
664 ..Default::default()
665 };
666 crate::format::format_date_at(secs, secs, &fmt)
667 } else {
668 v.to_string()
670 }
671}
672
673impl GridState {
674 #[must_use]
675 pub fn new(data: GridData, config: GridConfig, focus_handle: FocusHandle) -> Self {
676 let resolved_formats = Arc::new(config.resolve_all(&data.columns));
677 let col_count = data.columns.len();
678 let display_indices = Arc::new((0..data.rows.len()).collect::<Vec<_>>());
679 let display_rows = Arc::new(
680 display_indices
681 .iter()
682 .copied()
683 .enumerate()
684 .map(|(flat_row, source_row)| GridDisplayRow::Data {
685 source_row,
686 flat_row,
687 })
688 .collect(),
689 );
690 let data_rows = Arc::new(data.rows.clone());
691 let column_meta: Arc<[ColumnContext]> = data
692 .columns
693 .iter()
694 .enumerate()
695 .map(|(index, col)| ColumnContext {
696 index,
697 name: col.name.clone(),
698 kind: col.kind,
699 })
700 .collect();
701 Self {
702 data,
703 config,
704 window: None,
705 resolved_formats,
706 data_rows,
707 display_indices,
708 display_rows,
709 grouped_column: None,
710 row_groups: Arc::new(Vec::new()),
711 collapsed_group_labels: HashSet::new(),
712 selection: Selection::None,
713 range_anchor: None,
714 range_active: None,
715 sort: None,
716 filters: vec![ColumnFilter::default(); col_count],
717 scroll_handle: ScrollHandle::new(),
718 focus_handle,
719 bounds: Bounds::default(),
720 row_height: 24.0,
721 header_height: 32.0,
722 row_header_width: 50.0,
723 font_size: 14.0,
724 char_width: crate::grid::paint::default_char_width(14.0),
725 theme: GridTheme::default(),
726 theme_family: GridThemePair::default(),
727 is_dragging: false,
728 drag_start: None,
729 drag_start_hit: None,
730 scroll_at_click: None,
731 last_mouse_pos: None,
732 status_bar_height: 24.0,
733 debug_bar_enabled: false,
734 click_pos: None,
735 click_hit: None,
736 hover_hit: None,
737 resizing_col: None,
738 resize_start_x: 0.0,
739 resize_start_width: 0.0,
740 context_menu: None,
741 filter_panel: None,
742 pending_action: None,
743 pending_custom_context_menu_action: None,
744 context_menu_provider: None,
745 scrollbar_drag: None,
746 scrollbar_drag_start_offset: 0.0,
747 scrollbar_drag_start_pos: 0.0,
748 window_viewport: Size::default(),
749 edge_scroll_active: false,
750 column_meta,
751 self_weak: None,
752 busy: None,
753 }
754 }
755
756 pub fn set_config(&mut self, config: GridConfig) {
757 self.config = config;
758 self.rebuild_resolved_formats();
759 self.recompute();
760 }
761
762 pub fn append_rows(&mut self, rows: Vec<Vec<CellValue>>) -> Result<(), GridDataError> {
783 let expected = self.data.columns.len();
784 let base = self.data.rows.len();
785 for (offset, row) in rows.iter().enumerate() {
786 if row.len() != expected {
787 return Err(GridDataError::RaggedRow {
788 row_index: base + offset,
789 expected,
790 actual: row.len(),
791 });
792 }
793 }
794 if rows.is_empty() {
795 return Ok(());
796 }
797 Arc::make_mut(&mut self.data_rows).extend(rows.iter().cloned());
798 self.data.rows.extend(rows);
799 if self.sort.is_some()
800 || self.grouped_column.is_some()
801 || self.filters.iter().any(ColumnFilter::is_active)
802 {
803 self.recompute();
804 } else {
805 Arc::make_mut(&mut self.display_indices).extend(base..self.data.rows.len());
806 Arc::make_mut(&mut self.display_rows).extend((base..self.data.rows.len()).map(
807 |source_row| GridDisplayRow::Data {
808 source_row,
809 flat_row: source_row,
810 },
811 ));
812 }
813 if let Some(window) = &mut self.window {
814 window.total_rows += self.data.rows.len() - base;
815 }
816 Ok(())
817 }
818
819 #[must_use]
823 pub fn display_row_count(&self) -> usize {
824 self.window
825 .map(|w| w.total_rows)
826 .unwrap_or(self.display_rows.len())
827 }
828
829 #[must_use]
834 pub fn resident_row_for_display(&self, display_row: usize) -> Option<usize> {
835 match self.window {
836 Some(w) => display_row
837 .checked_sub(w.offset)
838 .filter(|r| *r < self.data.rows.len() && display_row < w.total_rows),
839 None => match self.display_rows.get(display_row) {
840 Some(GridDisplayRow::Data { source_row, .. }) => Some(*source_row),
841 _ => None,
842 },
843 }
844 }
845
846 #[must_use]
849 pub fn grouped_column(&self) -> Option<usize> {
850 self.grouped_column
851 }
852
853 pub fn set_grouped_column(&mut self, column: Option<usize>) {
857 if self.window.is_some() && column.is_some() {
858 return;
859 }
860 if let Some(column) = column {
861 if column >= self.data.columns.len() {
862 return;
863 }
864 }
865 if self.grouped_column != column {
866 self.grouped_column = column;
867 self.collapsed_group_labels.clear();
868 self.selection = Selection::None;
869 self.range_anchor = None;
870 self.range_active = None;
871 }
872 self.rebuild_display_rows();
873 self.clamp_scroll_to_bounds();
874 }
875
876 #[must_use]
878 pub fn row_groups(&self) -> &[RowGroup] {
879 &self.row_groups
880 }
881
882 pub fn set_group_collapsed(&mut self, group: usize, collapsed: bool) {
884 let Some(label) = self.row_groups.get(group).map(|group| group.label.clone()) else {
885 return;
886 };
887 if collapsed {
888 self.collapsed_group_labels.insert(label);
889 } else {
890 self.collapsed_group_labels.remove(&label);
891 }
892 self.selection = Selection::None;
893 self.range_anchor = None;
894 self.range_active = None;
895 self.clear_drag();
896 self.rebuild_display_rows();
897 self.clamp_scroll_to_bounds();
898 }
899
900 pub fn toggle_group(&mut self, group: usize) {
902 if let Some(section) = self.row_groups.get(group) {
903 self.set_group_collapsed(group, !section.collapsed);
904 }
905 }
906
907 pub fn set_row_window(
919 &mut self,
920 total_rows: usize,
921 offset: usize,
922 rows: Vec<Vec<CellValue>>,
923 ) -> Result<(), GridDataError> {
924 let expected = self.data.columns.len();
925 for (i, row) in rows.iter().enumerate() {
926 if row.len() != expected {
927 return Err(GridDataError::RaggedRow {
928 row_index: offset + i,
929 expected,
930 actual: row.len(),
931 });
932 }
933 }
934 self.sort = None;
935 self.grouped_column = None;
936 self.row_groups = Arc::new(Vec::new());
937 self.collapsed_group_labels.clear();
938 for filter in &mut self.filters {
939 *filter = ColumnFilter::default();
940 }
941 self.data_rows = Arc::new(rows.clone());
942 self.display_indices = Arc::new((0..rows.len()).collect());
943 self.data.rows = rows;
944 self.window = Some(RowWindow { total_rows, offset });
945 self.rebuild_display_rows();
946 Ok(())
947 }
948
949 #[must_use]
955 pub fn visible_row_range(&self) -> (usize, usize) {
956 let total = self.display_row_count();
957 let sy: f32 = self.scroll_handle.offset().y.into();
958 let vh: f32 = self.bounds.size.height.into();
959 let visible_h = vh - self.header_height;
960 if visible_h <= 0.0 || self.row_height <= 0.0 {
961 return (0, 0);
962 }
963 let first = ((sy / self.row_height) as usize).min(total);
964 let last = (first + (visible_h / self.row_height) as usize + 1).min(total);
965 (first, last)
966 }
967
968 pub fn set_debug_bar_enabled(&mut self, enabled: bool) {
972 self.debug_bar_enabled = enabled;
973 }
974
975 #[must_use]
978 pub fn is_busy(&self) -> bool {
979 self.busy.is_some()
980 }
981
982 #[must_use]
984 pub fn busy(&self) -> Option<&BusyState> {
985 self.busy.as_ref()
986 }
987
988 pub fn set_busy(&mut self, label: impl Into<String>) {
993 self.busy = Some(BusyState {
994 label: label.into(),
995 progress: None,
996 });
997 }
998
999 pub fn set_busy_progress(&mut self, progress: f32) {
1002 if let Some(b) = self.busy.as_mut() {
1003 b.progress = Some(progress.clamp(0.0, 1.0));
1004 }
1005 }
1006
1007 pub fn clear_busy(&mut self) {
1009 self.busy = None;
1010 }
1011
1012 pub fn spawn_background<R, W, D>(
1029 &mut self,
1030 cx: &mut App,
1031 label: impl Into<String>,
1032 work: W,
1033 on_done: D,
1034 ) where
1035 R: Send + 'static,
1036 W: FnOnce() -> R + Send + 'static,
1037 D: FnOnce(R, &mut GridState, &mut App) + 'static,
1038 {
1039 let Some(weak) = self.self_weak.clone() else {
1040 let result = work();
1042 on_done(result, self, cx);
1043 return;
1044 };
1045
1046 self.busy = Some(BusyState {
1047 label: label.into(),
1048 progress: None,
1049 });
1050
1051 let background = cx.background_executor().clone();
1052 cx.spawn(async move |cx| {
1053 let _ = cx.update(|app| {
1055 let _ = weak.update(app, |_s, c| c.notify());
1056 });
1057 let result = background.spawn(async move { work() }).await;
1058 let _ = cx.update(|app| {
1059 let _ = weak.update(app, |s, c| {
1060 s.busy = None;
1061 on_done(result, s, c);
1062 c.notify();
1063 });
1064 });
1065 })
1066 .detach();
1067 }
1068
1069 fn rebuild_resolved_formats(&mut self) {
1070 self.resolved_formats = Arc::new(self.config.resolve_all(&self.data.columns));
1071 }
1072
1073 pub fn recompute(&mut self) {
1074 if self.window.is_some() {
1077 self.display_indices = Arc::new((0..self.data.rows.len()).collect());
1078 self.rebuild_display_rows();
1079 return;
1080 }
1081 let mut indices: Vec<usize> = (0..self.data.rows.len())
1082 .filter(|&row_idx| {
1083 self.data.columns.iter().enumerate().all(|(col_idx, _col)| {
1084 let filter = &self.filters[col_idx];
1085 if !filter.is_active() {
1086 return true;
1087 }
1088 let cell = &self.data.rows[row_idx][col_idx];
1089 cell_passes_filter(cell, &self.resolved_formats[col_idx], filter)
1090 })
1091 })
1092 .collect();
1093
1094 if let Some((sort_col, direction)) = self.sort {
1095 indices.sort_by(|&a, &b| {
1096 let cell_a = &self.data.rows[a][sort_col];
1097 let cell_b = &self.data.rows[b][sort_col];
1098 let ord = compare_cells(cell_a, cell_b);
1099 match direction {
1100 SortDirection::Ascending => ord,
1101 SortDirection::Descending => ord.reverse(),
1102 }
1103 });
1104 }
1105 self.display_indices = Arc::new(indices);
1106 if self.grouped_column.is_some() {
1107 self.selection = Selection::None;
1108 self.range_anchor = None;
1109 self.range_active = None;
1110 self.clear_drag();
1111 }
1112 self.rebuild_display_rows();
1113 }
1114
1115 fn rebuild_display_rows(&mut self) {
1116 let Some(group_col) = self.grouped_column.filter(|_| self.window.is_none()) else {
1117 self.row_groups = Arc::new(Vec::new());
1118 self.display_rows = Arc::new(
1119 self.display_indices
1120 .iter()
1121 .copied()
1122 .enumerate()
1123 .map(|(flat_row, source_row)| GridDisplayRow::Data {
1124 source_row,
1125 flat_row,
1126 })
1127 .collect(),
1128 );
1129 return;
1130 };
1131
1132 let mut group_positions = HashMap::<String, usize>::new();
1133 let mut grouped_rows = Vec::<(String, Vec<(usize, usize)>)>::new();
1134 for (flat_row, &source_row) in self.display_indices.iter().enumerate() {
1135 let (label, _) = format_cell(
1136 &self.data.rows[source_row][group_col],
1137 &self.resolved_formats[group_col],
1138 );
1139 let group = *group_positions.entry(label.clone()).or_insert_with(|| {
1140 let index = grouped_rows.len();
1141 grouped_rows.push((label, Vec::new()));
1142 index
1143 });
1144 grouped_rows[group].1.push((source_row, flat_row));
1145 }
1146
1147 self.collapsed_group_labels
1148 .retain(|label| group_positions.contains_key(label));
1149 let groups: Vec<RowGroup> = grouped_rows
1150 .iter()
1151 .map(|(label, rows)| RowGroup {
1152 label: label.clone(),
1153 row_count: rows.len(),
1154 collapsed: self.collapsed_group_labels.contains(label),
1155 })
1156 .collect();
1157 let mut display_rows = Vec::with_capacity(
1158 groups.len()
1159 + groups
1160 .iter()
1161 .filter(|group| !group.collapsed)
1162 .map(|group| group.row_count)
1163 .sum::<usize>(),
1164 );
1165 for (group, (_, rows)) in grouped_rows.into_iter().enumerate() {
1166 display_rows.push(GridDisplayRow::GroupHeader { group });
1167 if !groups[group].collapsed {
1168 display_rows.extend(rows.into_iter().map(|(source_row, flat_row)| {
1169 GridDisplayRow::Data {
1170 source_row,
1171 flat_row,
1172 }
1173 }));
1174 }
1175 }
1176 self.row_groups = Arc::new(groups);
1177 self.display_rows = Arc::new(display_rows);
1178 }
1179
1180 fn content_size(&self) -> (f32, f32) {
1181 let cw: f32 = self.data.columns.iter().map(|c| c.width).sum();
1182 let ch = self.display_row_count() as f32 * self.row_height;
1183 (cw, ch)
1184 }
1185
1186 pub(crate) fn max_scroll(&self) -> (f32, f32) {
1187 let (cw, ch) = self.content_size();
1188 let (rw, rh) = self.scrollbar_reserved();
1189 let vw: f32 = self.bounds.size.width.into();
1190 let vh: f32 = self.bounds.size.height.into();
1191 let vw = vw - self.row_header_width - rw;
1192 let vh = vh - self.header_height - rh;
1193 ((cw - vw).max(0.0), (ch - vh).max(0.0))
1194 }
1195
1196 pub(crate) fn clamp_scroll_to_bounds(&mut self) {
1202 let (mx, my) = self.max_scroll();
1203 let s = self.scroll_handle.offset();
1204 let nx = f32::from(s.x).clamp(0.0, mx);
1205 let ny = f32::from(s.y).clamp(0.0, my);
1206 if nx != f32::from(s.x) || ny != f32::from(s.y) {
1207 self.scroll_handle.set_offset(Point {
1208 x: px(nx),
1209 y: px(ny),
1210 });
1211 }
1212 }
1213
1214 fn scrollbar_reserved(&self) -> (f32, f32) {
1215 let (cw, ch) = self.content_size();
1216 let vw: f32 = self.bounds.size.width.into();
1217 let vh: f32 = self.bounds.size.height.into();
1218 let vw = vw - self.row_header_width;
1219 let vh = vh - self.header_height;
1220 let reserved_w = if ch > vh { SCROLLBAR_SIZE } else { 0.0 };
1221 let reserved_h = if cw > vw { SCROLLBAR_SIZE } else { 0.0 };
1222 (reserved_w, reserved_h)
1223 }
1224
1225 fn vbar_geom(&self) -> Option<(f32, f32, f32, f32, f32)> {
1226 let (_, ch) = self.content_size();
1227 let (_, rh) = self.scrollbar_reserved();
1228 let vh: f32 = self.bounds.size.height.into();
1229 let vh = vh - self.header_height - rh;
1230 if ch <= vh {
1231 return None;
1232 }
1233 let sw: f32 = self.bounds.size.width.into();
1236 let sh: f32 = self.bounds.size.height.into();
1237 let track_x = sw - SCROLLBAR_SIZE;
1238 let track_y = self.header_height;
1239 let track_h = sh - self.header_height - rh;
1240 let thumb_h = ((track_h * (vh / ch)).max(20.0)).min(track_h);
1241 Some((track_x, track_y, SCROLLBAR_SIZE, track_h, thumb_h))
1242 }
1243
1244 fn hbar_geom(&self) -> Option<(f32, f32, f32, f32, f32)> {
1245 let (cw, _) = self.content_size();
1246 let (rw, _) = self.scrollbar_reserved();
1247 let vw: f32 = self.bounds.size.width.into();
1248 let vw = vw - self.row_header_width - rw;
1249 if cw <= vw {
1250 return None;
1251 }
1252 let sw: f32 = self.bounds.size.width.into();
1255 let sh: f32 = self.bounds.size.height.into();
1256 let track_x = self.row_header_width;
1257 let track_y = sh - SCROLLBAR_SIZE;
1258 let track_w = sw - self.row_header_width - rw;
1259 let thumb_w = ((track_w * (vw / cw)).max(20.0)).min(track_w);
1260 Some((track_x, track_y, track_w, SCROLLBAR_SIZE, thumb_w))
1261 }
1262
1263 pub(crate) fn scroll_to_vbar(&mut self, mouse_y: f32) {
1264 if let Some((_, track_y, _, track_h, thumb_h)) = self.vbar_geom() {
1265 let (_, max_y) = self.max_scroll();
1266 let range = (track_h - thumb_h).max(0.0);
1267 let rel = (mouse_y - track_y - thumb_h * 0.5).clamp(0.0, range);
1268 let frac = if range > 0.0 { rel / range } else { 0.0 };
1269 let new_y = frac * max_y;
1270 let x = self.scroll_handle.offset().x;
1271 self.scroll_handle.set_offset(Point { x, y: px(new_y) });
1272 }
1273 }
1274
1275 pub(crate) fn scroll_to_hbar(&mut self, mouse_x: f32) {
1276 if let Some((track_x, _, track_w, _, thumb_w)) = self.hbar_geom() {
1277 let (max_x, _) = self.max_scroll();
1278 let range = (track_w - thumb_w).max(0.0);
1279 let rel = (mouse_x - track_x - thumb_w * 0.5).clamp(0.0, range);
1280 let frac = if range > 0.0 { rel / range } else { 0.0 };
1281 let new_x = frac * max_x;
1282 let y = self.scroll_handle.offset().y;
1283 self.scroll_handle.set_offset(Point { x: px(new_x), y });
1284 }
1285 }
1286
1287 pub(crate) fn scroll_one_edge_tick(&mut self, dx: f32, dy: f32) {
1288 let (mx, my) = self.max_scroll();
1289 let s = self.scroll_handle.offset();
1290 let new_x: f32 = (f32::from(s.x) + dx).clamp(0.0, mx);
1291 let new_y: f32 = (f32::from(s.y) + dy).clamp(0.0, my);
1292 self.scroll_handle.set_offset(Point {
1293 x: px(new_x),
1294 y: px(new_y),
1295 });
1296 }
1297
1298 pub fn toggle_sort(&mut self, col: usize) {
1299 if self.window.is_some() {
1302 return;
1303 }
1304 self.sort = match self.sort {
1305 Some((c, SortDirection::Ascending)) if c == col => {
1306 Some((col, SortDirection::Descending))
1307 }
1308 Some((c, SortDirection::Descending)) if c == col => None,
1309 _ => Some((col, SortDirection::Ascending)),
1310 };
1311 self.recompute();
1312 }
1313
1314 pub fn handle_mouse_down(&mut self, pos: Point<Pixels>, shift: bool) {
1315 self.handle_mouse_down_with_modifiers(pos, shift, false);
1316 }
1317
1318 pub fn handle_mouse_down_with_modifiers(&mut self, pos: Point<Pixels>, shift: bool, cmd: bool) {
1319 let hit = self.hit_test(pos);
1320 self.click_pos = Some(pos);
1321 self.click_hit = Some(hit);
1322 match hit {
1323 HitResult::VerticalScrollbar => {
1324 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1325 self.scroll_to_vbar(f32::from(pos.y));
1326 self.clear_drag();
1327 }
1328 HitResult::HorizontalScrollbar => {
1329 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1330 self.scroll_to_hbar(f32::from(pos.x));
1331 self.clear_drag();
1332 }
1333 HitResult::ColumnBorder(col) => {
1334 self.resizing_col = Some(col);
1335 self.resize_start_x = f32::from(pos.x);
1336 self.resize_start_width = self.data.columns[col].width;
1337 self.clear_drag();
1338 }
1339 HitResult::ColumnHeader(col) => {
1340 if cmd {
1341 let mut cols: Vec<usize> = match &self.selection {
1344 Selection::Column(c) => vec![*c],
1345 Selection::Columns(cs) => cs.clone(),
1346 _ => Vec::new(),
1347 };
1348 if let Some(idx) = cols.iter().position(|&c| c == col) {
1349 cols.remove(idx);
1350 } else {
1351 cols.push(col);
1352 cols.sort_unstable();
1353 }
1354 self.selection = match cols.len() {
1355 0 => Selection::None,
1356 1 => Selection::Column(cols[0]),
1357 _ => Selection::Columns(cols),
1358 };
1359 self.clear_drag();
1360 } else {
1361 self.selection = Selection::Column(col);
1362 self.start_drag(pos);
1364 self.drag_start_hit = Some(HitResult::ColumnHeader(col));
1365 }
1366 }
1367 HitResult::SortButton(col) => {
1368 self.toggle_sort(col);
1371 self.clear_drag();
1372 }
1373 HitResult::ContextMenuItem(_) => {}
1374 HitResult::GroupHeader(group) => {
1375 self.toggle_group(group);
1376 self.clear_drag();
1377 }
1378 HitResult::RowHeader(row) => {
1379 if cmd {
1380 let mut rows: Vec<usize> = match &self.selection {
1383 Selection::Row(r) => vec![*r],
1384 Selection::RowRange(r1, r2) => (*r1.min(r2)..=*r1.max(r2)).collect(),
1385 Selection::Rows(rs) => rs.clone(),
1386 _ => Vec::new(),
1387 };
1388 if let Some(idx) = rows.iter().position(|&r| r == row) {
1389 rows.remove(idx);
1390 } else {
1391 rows.push(row);
1392 rows.sort_unstable();
1393 }
1394 self.selection = match rows.len() {
1395 0 => Selection::None,
1396 1 => Selection::Row(rows[0]),
1397 _ => Selection::Rows(rows),
1398 };
1399 self.clear_drag();
1400 return;
1401 }
1402 self.selection = if shift {
1403 if let Selection::Row(prev) = self.selection {
1404 let (s, e) = (prev, row);
1405 Selection::RowRange(s.min(e), s.max(e))
1406 } else {
1407 Selection::Row(row)
1408 }
1409 } else {
1410 Selection::Row(row)
1411 };
1412 self.start_drag(pos);
1413 self.drag_start_hit = Some(HitResult::RowHeader(row));
1414 }
1415 HitResult::Cell(row, col) => {
1416 if cmd {
1417 let mut cells: Vec<(usize, usize)> = match &self.selection {
1420 Selection::Cell(r, c) => vec![(*r, *c)],
1421 Selection::Cells(cs) => cs.clone(),
1422 _ => Vec::new(),
1423 };
1424 if let Some(idx) = cells.iter().position(|&rc| rc == (row, col)) {
1425 cells.remove(idx);
1426 } else {
1427 cells.push((row, col));
1428 cells.sort_unstable();
1429 }
1430 self.selection = match cells.len() {
1431 0 => Selection::None,
1432 1 => Selection::Cell(cells[0].0, cells[0].1),
1433 _ => Selection::Cells(cells),
1434 };
1435 self.range_anchor = None;
1436 self.range_active = None;
1437 self.clear_drag();
1438 return;
1439 }
1440 self.selection = if shift {
1441 let anchor = self
1443 .range_anchor
1444 .or(match self.selection {
1445 Selection::Cell(pr, pc) => Some((pr, pc)),
1446 _ => None,
1447 })
1448 .unwrap_or((row, col));
1449 self.range_anchor = Some(anchor);
1450 self.range_active = Some((row, col));
1451 Selection::CellRange(
1452 anchor.0.min(row),
1453 anchor.1.min(col),
1454 anchor.0.max(row),
1455 anchor.1.max(col),
1456 )
1457 } else {
1458 self.range_anchor = Some((row, col));
1459 self.range_active = Some((row, col));
1460 Selection::Cell(row, col)
1461 };
1462 self.start_drag(pos);
1463 self.drag_start_hit = Some(HitResult::Cell(row, col));
1464 }
1465 HitResult::Corner | HitResult::None => {
1466 self.selection = Selection::None;
1467 self.range_anchor = None;
1468 self.range_active = None;
1469 self.context_menu = None;
1470 self.filter_panel = None;
1471 self.clear_drag();
1472 }
1473 }
1474 }
1475
1476 fn start_drag(&mut self, pos: Point<Pixels>) {
1477 self.is_dragging = false;
1478 self.drag_start = Some(pos);
1479 self.scroll_at_click = Some(self.scroll_handle.offset());
1480 self.last_mouse_pos = Some(pos);
1481 }
1482
1483 pub(crate) fn open_context_menu(&mut self, col: usize, anchor: Point<Pixels>) {
1484 self.context_menu = Some(menu_mod::ContextMenu::standard(col, anchor));
1485 self.filter_panel = None;
1486 }
1487
1488 pub(crate) fn context_menu_target_from_hit(&self, hit: HitResult) -> Option<ContextMenuTarget> {
1491 match hit {
1492 HitResult::Cell(row, col) => {
1493 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1494 Some(ContextMenuTarget::Cell {
1495 display_row_index: row,
1496 source_row_index: source_row,
1497 column_index: col,
1498 })
1499 }
1500 HitResult::RowHeader(row) => {
1501 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1502 Some(ContextMenuTarget::RowHeader {
1503 display_row_index: row,
1504 source_row_index: source_row,
1505 })
1506 }
1507 HitResult::ColumnHeader(col) => {
1508 Some(ContextMenuTarget::ColumnHeader { column_index: col })
1509 }
1510 HitResult::SortButton(col) => Some(ContextMenuTarget::SortButton { column_index: col }),
1511 _ => None,
1512 }
1513 }
1514
1515 pub(crate) fn effective_selection_for_context_target(
1520 &self,
1521 target: &ContextMenuTarget,
1522 ) -> Selection {
1523 match target {
1524 ContextMenuTarget::Cell {
1525 display_row_index,
1526 column_index,
1527 ..
1528 } => {
1529 if is_cell_selected(&self.selection, *display_row_index, *column_index) {
1530 self.selection.clone()
1531 } else {
1532 Selection::Cell(*display_row_index, *column_index)
1533 }
1534 }
1535 ContextMenuTarget::RowHeader {
1536 display_row_index, ..
1537 } => {
1538 if is_row_selected(&self.selection, *display_row_index) {
1539 self.selection.clone()
1540 } else {
1541 Selection::Row(*display_row_index)
1542 }
1543 }
1544 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. } => {
1545 self.selection.clone()
1546 }
1547 }
1548 }
1549
1550 pub(crate) fn build_context_menu_request(
1562 &self,
1563 target: ContextMenuTarget,
1564 selection: &Selection,
1565 ) -> ContextMenuRequest {
1566 let mut target = target;
1573 let mut selection = selection.clone();
1574 if let Some(w) = self.window {
1575 let resident_last = self.data.rows.len().saturating_sub(1);
1576 let to_resident = |dr: usize| dr.saturating_sub(w.offset).min(resident_last);
1577 target = match target {
1578 ContextMenuTarget::Cell {
1579 display_row_index,
1580 source_row_index,
1581 column_index,
1582 } => ContextMenuTarget::Cell {
1583 display_row_index: to_resident(display_row_index),
1584 source_row_index,
1585 column_index,
1586 },
1587 ContextMenuTarget::RowHeader {
1588 display_row_index,
1589 source_row_index,
1590 } => ContextMenuTarget::RowHeader {
1591 display_row_index: to_resident(display_row_index),
1592 source_row_index,
1593 },
1594 other => other,
1595 };
1596 selection = match selection {
1597 Selection::Cell(r, c) => Selection::Cell(to_resident(r), c),
1598 Selection::Row(r) => Selection::Row(to_resident(r)),
1599 Selection::CellRange(r1, c1, r2, c2) => {
1600 Selection::CellRange(to_resident(r1), c1, to_resident(r2), c2)
1601 }
1602 other => other,
1603 };
1604 }
1605
1606 let request_display_indices = if self.grouped_column.is_some() && self.window.is_none() {
1607 let mut row_map = vec![None; self.display_rows.len()];
1608 let mut indices = Vec::new();
1609 for (display_row, row) in self.display_rows.iter().enumerate() {
1610 if let GridDisplayRow::Data { source_row, .. } = row {
1611 row_map[display_row] = Some(indices.len());
1612 indices.push(*source_row);
1613 }
1614 }
1615 let map_row = |display_row: usize| {
1616 row_map
1617 .get(display_row)
1618 .copied()
1619 .flatten()
1620 .or_else(|| {
1621 row_map
1622 .iter()
1623 .skip(display_row.saturating_add(1))
1624 .flatten()
1625 .copied()
1626 .next()
1627 })
1628 .or_else(|| {
1629 row_map
1630 .iter()
1631 .take(display_row)
1632 .rev()
1633 .flatten()
1634 .copied()
1635 .next()
1636 })
1637 .unwrap_or(0)
1638 };
1639 target = match target {
1640 ContextMenuTarget::Cell {
1641 display_row_index,
1642 source_row_index,
1643 column_index,
1644 } => ContextMenuTarget::Cell {
1645 display_row_index: map_row(display_row_index),
1646 source_row_index,
1647 column_index,
1648 },
1649 ContextMenuTarget::RowHeader {
1650 display_row_index,
1651 source_row_index,
1652 } => ContextMenuTarget::RowHeader {
1653 display_row_index: map_row(display_row_index),
1654 source_row_index,
1655 },
1656 other => other,
1657 };
1658 selection = match selection {
1659 Selection::Cell(row, col) => Selection::Cell(map_row(row), col),
1660 Selection::Row(row) => Selection::Row(map_row(row)),
1661 Selection::CellRange(r1, c1, r2, c2) => {
1662 Selection::CellRange(map_row(r1), c1, map_row(r2), c2)
1663 }
1664 Selection::RowRange(r1, r2) => Selection::RowRange(map_row(r1), map_row(r2)),
1665 Selection::Rows(rows) => Selection::Rows(
1666 rows.into_iter()
1667 .filter_map(|row| row_map.get(row).copied().flatten())
1668 .collect(),
1669 ),
1670 Selection::Cells(cells) => Selection::Cells(
1671 cells
1672 .into_iter()
1673 .filter_map(|(row, col)| {
1674 row_map.get(row).copied().flatten().map(|row| (row, col))
1675 })
1676 .collect(),
1677 ),
1678 other => other,
1679 };
1680 Arc::new(indices)
1681 } else {
1682 Arc::clone(&self.display_indices)
1683 };
1684 let selection = &selection;
1685
1686 let nrows = request_display_indices.len();
1687 let ncols = self.data.columns.len();
1688
1689 let (r1, c1, r2, c2) = match selection.normalized_bounds() {
1690 Some((r1, c1, r2, c2)) => {
1691 let r1 = r1.min(nrows.saturating_sub(1));
1692 let r2 = r2.min(nrows.saturating_sub(1));
1693 let c1 = c1.min(ncols.saturating_sub(1));
1694 let c2 = c2.min(ncols.saturating_sub(1));
1695 (r1, c1, r2, c2)
1696 }
1697 None => match &target {
1698 ContextMenuTarget::Cell {
1699 display_row_index,
1700 column_index,
1701 ..
1702 } => (
1703 *display_row_index,
1704 *column_index,
1705 *display_row_index,
1706 *column_index,
1707 ),
1708 ContextMenuTarget::RowHeader {
1709 display_row_index, ..
1710 } => (
1711 *display_row_index,
1712 0,
1713 *display_row_index,
1714 ncols.saturating_sub(1),
1715 ),
1716 ContextMenuTarget::ColumnHeader { column_index }
1717 | ContextMenuTarget::SortButton { column_index } => {
1718 (0, *column_index, nrows.saturating_sub(1), *column_index)
1719 }
1720 },
1721 };
1722
1723 let menu_selection = ContextMenuSelection {
1724 row_start: r1,
1725 row_end: r2,
1726 column_start: c1,
1727 column_end: c2,
1728 };
1729
1730 let column_oriented =
1735 matches!(
1736 target,
1737 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. }
1738 ) || matches!(selection, Selection::Column(_) | Selection::Columns(_));
1739
1740 ContextMenuRequest::new(
1741 target,
1742 Some(menu_selection),
1743 Arc::clone(&self.data_rows),
1744 request_display_indices,
1745 Arc::clone(&self.column_meta),
1746 column_oriented,
1747 )
1748 }
1749
1750 pub(crate) fn execute_custom_context_menu_action(
1754 &mut self,
1755 pending: PendingCustomContextMenuAction,
1756 cx: &mut App,
1757 ) {
1758 self.context_menu = None;
1759 self.filter_panel = None;
1760
1761 let Some(provider) = self.context_menu_provider.clone() else {
1762 return;
1763 };
1764
1765 provider.on_action(&pending.id, &pending.request, self, cx);
1766 }
1767
1768 pub(crate) fn convert_context_menu_items(items: Vec<ContextMenuItem>) -> Vec<MenuItem> {
1771 items
1772 .into_iter()
1773 .map(|item| match item {
1774 ContextMenuItem::BuiltIn(action) => MenuItem::Action(action),
1775 ContextMenuItem::Action { id, label } => MenuItem::Custom { id, label },
1776 ContextMenuItem::Separator => MenuItem::Separator,
1777 })
1778 .collect()
1779 }
1780
1781 pub fn execute_action(&mut self, action: MenuAction, col: usize, cx: &mut App) {
1782 match action {
1783 MenuAction::SelectColumn => {
1784 self.selection = Selection::Column(col);
1785 }
1786 MenuAction::CopyColumn => {
1787 let text = self.column_text(col);
1788 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1789 }
1790 MenuAction::CopyColumnWithHeaders => {
1791 let mut text = String::new();
1792 text.push_str(&self.data.columns[col].name);
1793 text.push('\n');
1794 text.push_str(&self.column_text(col));
1795 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1796 }
1797 MenuAction::SortAscending => {
1798 self.sort = Some((col, SortDirection::Ascending));
1799 self.recompute();
1800 }
1801 MenuAction::SortDescending => {
1802 self.sort = Some((col, SortDirection::Descending));
1803 self.recompute();
1804 }
1805 MenuAction::ClearSort => {
1806 self.sort = None;
1807 self.recompute();
1808 }
1809 MenuAction::GroupBy => self.set_grouped_column(Some(col)),
1810 MenuAction::ClearGrouping => self.set_grouped_column(None),
1811 MenuAction::FilterPrompt => {
1812 let anchor = self.context_menu.as_ref().map(|m| m.anchor);
1813 self.open_filter_panel(col, anchor);
1814 }
1815 MenuAction::ClearFilter => {
1816 if col < self.filters.len() {
1817 self.filters[col] = ColumnFilter::default();
1818 self.recompute();
1819 }
1820 }
1821 }
1822 self.context_menu = None;
1823 }
1824
1825 pub fn open_filter_panel(&mut self, col: usize, _anchor: Option<Point<Pixels>>) {
1836 if col >= self.data.columns.len() {
1837 return;
1838 }
1839 let sx = f32::from(self.scroll_handle.offset().x);
1840 let col_x = self.row_header_width
1841 + self.data.columns[..col]
1842 .iter()
1843 .map(|c| c.width)
1844 .sum::<f32>()
1845 - sx;
1846 let anchor = Point {
1847 x: px(col_x + self.data.columns[col].width * 0.5),
1848 y: px(0.0),
1849 };
1850 let kind = self.data.columns[col].kind;
1851 let existing = self.filters.get(col).cloned().unwrap_or_default();
1852
1853 let distinct = {
1855 let fmt = &self.resolved_formats[col];
1856 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1857 let mut pairs: Vec<(String, &CellValue)> = Vec::new();
1858 for row in &self.data.rows {
1859 let cell = &row[col];
1860 let (label, _) = format_cell(cell, fmt);
1861 if seen.insert(label.clone()) {
1862 pairs.push((label, cell));
1863 }
1864 }
1865 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
1866 pairs
1867 .into_iter()
1868 .map(|(label, _)| {
1869 let checked = match &existing.values {
1870 None => true,
1871 Some(set) => set.contains(&label),
1872 };
1873 FilterValueRow { label, checked }
1874 })
1875 .collect()
1876 };
1877
1878 let (op_index, operand_a, operand_b) = seed_operator(kind, &existing.predicate);
1879
1880 self.context_menu = None;
1881 self.filter_panel = Some(FilterPanel {
1882 col,
1883 anchor,
1884 kind,
1885 search: TextInput::default(),
1886 op_index,
1887 op_menu_open: false,
1888 operand_a: TextInput::new(operand_a),
1889 operand_b: TextInput::new(operand_b),
1890 focus: FilterInput::Search,
1891 auto_apply: true,
1892 distinct,
1893 });
1894 }
1895
1896 pub fn apply_filter_panel(&mut self) {
1899 let Some(panel) = &self.filter_panel else {
1900 return;
1901 };
1902 let col = panel.col;
1903 let filter = panel.to_filter();
1904 if col < self.filters.len() {
1905 self.filters[col] = filter;
1906 self.recompute();
1907 }
1908 }
1909
1910 pub fn maybe_auto_apply(&mut self) {
1912 if self.filter_panel.is_some() {
1913 self.apply_filter_panel();
1914 }
1915 }
1916
1917 pub fn clear_filter_panel(&mut self) {
1920 let mut target_col = None;
1921 if let Some(panel) = &mut self.filter_panel {
1922 panel.op_index = 0;
1923 panel.op_menu_open = false;
1924 panel.operand_a = TextInput::default();
1925 panel.operand_b = TextInput::default();
1926 panel.search = TextInput::default();
1927 for row in &mut panel.distinct {
1928 row.checked = true;
1929 }
1930 target_col = Some(panel.col);
1931 }
1932 if let Some(col) = target_col {
1933 if col < self.filters.len() {
1934 self.filters[col] = ColumnFilter::default();
1935 }
1936 }
1937 self.recompute();
1938 }
1939
1940 pub fn set_panel_sort(&mut self, direction: SortDirection) {
1943 if let Some(panel) = &self.filter_panel {
1944 let col = panel.col;
1945 self.sort = match self.sort {
1946 Some((c, d)) if c == col && d == direction => None,
1947 _ => Some((col, direction)),
1948 };
1949 self.recompute();
1950 }
1951 }
1952
1953 pub fn toggle_filter_value(&mut self, index: usize) {
1956 if let Some(panel) = &mut self.filter_panel {
1957 if let Some(row) = panel.distinct.get_mut(index) {
1958 row.checked = !row.checked;
1959 }
1960 }
1961 self.maybe_auto_apply();
1962 }
1963
1964 pub fn toggle_filter_select_all(&mut self) {
1969 if let Some(panel) = &mut self.filter_panel {
1970 let target = !panel.all_checked();
1971 for row in &mut panel.distinct {
1972 row.checked = target;
1973 }
1974 }
1975 self.maybe_auto_apply();
1976 }
1977
1978 pub fn set_filter_operator(&mut self, op_index: usize) {
1981 if let Some(panel) = &mut self.filter_panel {
1982 panel.op_index = op_index;
1983 panel.op_menu_open = false;
1984 if op_index != 0 {
1985 panel.focus = FilterInput::OperandA;
1986 }
1987 }
1988 self.maybe_auto_apply();
1989 }
1990
1991 pub fn toggle_filter_op_menu(&mut self) {
1993 if let Some(panel) = &mut self.filter_panel {
1994 panel.op_menu_open = !panel.op_menu_open;
1995 }
1996 }
1997
1998 pub fn set_filter_focus(&mut self, focus: FilterInput) {
2000 if let Some(panel) = &mut self.filter_panel {
2001 panel.focus = focus;
2002 }
2003 }
2004
2005 pub fn toggle_filter_auto_apply(&mut self) {
2007 if let Some(panel) = &mut self.filter_panel {
2008 panel.auto_apply = !panel.auto_apply;
2009 }
2010 self.maybe_auto_apply();
2011 }
2012
2013 fn column_text(&self, col: usize) -> String {
2014 let mut text = String::new();
2015 let fmt = &self.resolved_formats[col];
2016 for &row_idx in self.display_indices.iter() {
2017 let cell = &self.data.rows[row_idx][col];
2018 let (s, _) = format_cell(cell, fmt);
2019 text.push_str(&s);
2020 text.push('\n');
2021 }
2022 text
2023 }
2024
2025 fn clear_drag(&mut self) {
2026 self.is_dragging = false;
2027 self.drag_start = None;
2028 self.drag_start_hit = None;
2029 self.scroll_at_click = None;
2030 }
2031
2032 fn drag_world_corners(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
2033 let start = self.drag_start?;
2034 let mouse = self.last_mouse_pos?;
2035 let click_scroll = self
2036 .scroll_at_click
2037 .unwrap_or_else(|| self.scroll_handle.offset());
2038 let scroll = self.scroll_handle.offset();
2039 let sx_click: f32 = click_scroll.x.into();
2040 let sy_click: f32 = click_scroll.y.into();
2041 let sx: f32 = scroll.x.into();
2042 let sy: f32 = scroll.y.into();
2043 let sx0: f32 = start.x.into();
2044 let sy0: f32 = start.y.into();
2045 let mx: f32 = mouse.x.into();
2046 let my: f32 = mouse.y.into();
2047 let start_world = Point {
2048 x: px(sx0 + sx_click),
2049 y: px(sy0 + sy_click),
2050 };
2051 let end_world = Point {
2052 x: px(mx + sx),
2053 y: px(my + sy),
2054 };
2055 Some((start_world, end_world))
2056 }
2057
2058 pub fn drag_screen_rect(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
2059 if !self.is_dragging {
2060 return None;
2061 }
2062 let (start_world, end_world) = self.drag_world_corners()?;
2063 let scroll = self.scroll_handle.offset();
2064 let sx: f32 = scroll.x.into();
2065 let sy: f32 = scroll.y.into();
2066 let start_screen = Point {
2067 x: px(f32::from(start_world.x) - sx),
2068 y: px(f32::from(start_world.y) - sy),
2069 };
2070 let end_screen = Point {
2071 x: px(f32::from(end_world.x) - sx),
2072 y: px(f32::from(end_world.y) - sy),
2073 };
2074 Some((start_screen, end_screen))
2075 }
2076
2077 fn update_drag(&mut self) {
2078 let (start_world, end_world) = match self.drag_world_corners() {
2079 Some(c) => c,
2080 None => return,
2081 };
2082 if !self.is_dragging {
2083 let dx = f32::from(end_world.x) - f32::from(start_world.x);
2084 let dy = f32::from(end_world.y) - f32::from(start_world.y);
2085 if dx * dx + dy * dy <= 400.0 {
2086 return;
2087 }
2088 self.is_dragging = true;
2089 }
2090 let r1 = match self.drag_start_hit {
2091 Some(h) => h,
2092 None => return,
2093 };
2094 let r2 = self.hit_test_content(f32::from(end_world.x), f32::from(end_world.y), 0.0, 0.0);
2098 match (r1, r2) {
2099 (HitResult::Cell(r1c, c1), HitResult::Cell(r2c, c2)) => {
2100 self.selection =
2101 Selection::CellRange(r1c.min(r2c), c1.min(c2), r1c.max(r2c), c1.max(c2));
2102 }
2103 (HitResult::RowHeader(r1r), HitResult::RowHeader(r2r)) => {
2104 self.selection = Selection::RowRange(r1r.min(r2r), r1r.max(r2r));
2105 }
2106 (
2107 HitResult::ColumnHeader(c1),
2108 HitResult::ColumnHeader(c2)
2109 | HitResult::SortButton(c2)
2110 | HitResult::ColumnBorder(c2),
2111 ) => {
2112 self.selection = if c1 == c2 {
2113 Selection::Column(c1)
2114 } else {
2115 Selection::Columns((c1.min(c2)..=c1.max(c2)).collect())
2116 };
2117 }
2118 _ => {}
2119 }
2120 }
2121
2122 fn update_drag_from_last(&mut self) {
2123 self.update_drag();
2124 }
2125
2126 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, pressed_button: Option<MouseButton>) {
2127 if self.is_dragging && pressed_button != Some(MouseButton::Left) {
2128 self.handle_mouse_up();
2129 return;
2130 }
2131 if let Some(col) = self.resizing_col {
2132 if pressed_button != Some(MouseButton::Left) {
2133 self.resizing_col = None;
2134 return;
2135 }
2136 let new_w =
2137 (self.resize_start_width + (f32::from(pos.x) - self.resize_start_x)).max(40.0);
2138 self.data.columns[col].width = new_w;
2139 return;
2140 }
2141 if let Some(axis) = self.scrollbar_drag {
2142 if pressed_button != Some(MouseButton::Left) {
2143 self.scrollbar_drag = None;
2144 return;
2145 }
2146 match axis {
2147 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
2148 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
2149 }
2150 self.last_mouse_pos = Some(pos);
2151 return;
2152 }
2153 self.last_mouse_pos = Some(pos);
2154 if self.context_menu.is_some() {
2155 return;
2160 }
2161 self.hover_hit = Some(self.hit_test(pos));
2162 if self.drag_start.is_none() {
2163 return;
2164 }
2165 self.update_drag();
2166 }
2167
2168 pub fn handle_scroll_drag(&mut self) {
2169 if self.drag_start.is_some() && self.last_mouse_pos.is_some() {
2170 self.update_drag();
2171 }
2172 }
2173
2174 pub fn handle_mouse_up(&mut self) {
2175 self.resizing_col = None;
2176 self.scrollbar_drag = None;
2177 self.clear_drag();
2178 }
2179
2180 pub fn apply_edge_scroll(&mut self) -> bool {
2181 apply_edge_scroll(self)
2182 }
2183
2184 pub fn select_all(&mut self) {
2185 let nrows = self.display_row_count();
2186 let ncols = self.data.columns.len();
2187 if nrows > 0 && ncols > 0 {
2188 self.selection = Selection::CellRange(0, 0, nrows - 1, ncols - 1);
2189 }
2190 }
2191
2192 #[must_use]
2196 pub fn selected_rows(&self) -> Vec<usize> {
2197 let nrows = self.display_row_count();
2198 let rows = match &self.selection {
2199 Selection::Row(r) if *r < nrows => vec![*r],
2200 Selection::RowRange(r1, r2) => {
2201 (*r1.min(r2)..=*r1.max(r2)).filter(|&r| r < nrows).collect()
2202 }
2203 Selection::Rows(rows) => rows.iter().copied().filter(|&r| r < nrows).collect(),
2204 _ => Vec::new(),
2205 };
2206 rows.into_iter()
2207 .filter(|&row| self.is_data_display_row(row))
2208 .collect()
2209 }
2210
2211 #[must_use]
2215 pub fn selected_columns(&self) -> Vec<usize> {
2216 let ncols = self.data.columns.len();
2217 match &self.selection {
2218 Selection::Column(c) if *c < ncols => vec![*c],
2219 Selection::Columns(cols) => cols.iter().copied().filter(|&c| c < ncols).collect(),
2220 _ => Vec::new(),
2221 }
2222 }
2223
2224 #[must_use]
2229 pub fn selected_cells(&self) -> Vec<(usize, usize)> {
2230 let nrows = self.display_row_count();
2231 let ncols = self.data.columns.len();
2232 if nrows == 0 || ncols == 0 {
2233 return Vec::new();
2234 }
2235 match &self.selection {
2236 Selection::None => Vec::new(),
2237 Selection::Cell(r, c) if *r < nrows && *c < ncols && self.is_data_display_row(*r) => {
2238 vec![(*r, *c)]
2239 }
2240 Selection::Cell(..) => Vec::new(),
2241 Selection::Cells(cells) => cells
2242 .iter()
2243 .copied()
2244 .filter(|&(r, c)| r < nrows && c < ncols && self.is_data_display_row(r))
2245 .collect(),
2246 Selection::Column(_) | Selection::Columns(_) => {
2247 let cols = self.selected_columns();
2248 (0..nrows)
2249 .filter(|&r| self.is_data_display_row(r))
2250 .flat_map(|r| cols.iter().map(move |&c| (r, c)))
2251 .collect()
2252 }
2253 Selection::Row(_) | Selection::RowRange(..) | Selection::Rows(_) => self
2254 .selected_rows()
2255 .into_iter()
2256 .flat_map(|r| (0..ncols).map(move |c| (r, c)))
2257 .collect(),
2258 Selection::CellRange(r1, c1, r2, c2) => {
2259 let (rmin, rmax) = (*r1.min(r2), *r1.max(r2));
2260 let (cmin, cmax) = (*c1.min(c2), *c1.max(c2));
2261 (rmin..=rmax.min(nrows.saturating_sub(1)))
2262 .filter(|&r| self.is_data_display_row(r))
2263 .flat_map(|r| (cmin..=cmax.min(ncols.saturating_sub(1))).map(move |c| (r, c)))
2264 .collect()
2265 }
2266 }
2267 }
2268
2269 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
2270 let Some((raw_r1, raw_c1, raw_r2, raw_c2)) = self.selection.normalized_bounds() else {
2271 return;
2272 };
2273 if self.display_row_count() == 0 || self.data.columns.is_empty() {
2274 return;
2275 }
2276 let last_row = self.display_row_count() - 1;
2277 let last_col = self.data.columns.len() - 1;
2278 let r1 = raw_r1.min(last_row);
2279 let r2 = raw_r2.min(last_row);
2280 let c1 = raw_c1.min(last_col);
2281 let c2 = raw_c2.min(last_col);
2282 let mut text = String::new();
2283 if with_headers {
2284 for c in c1..=c2 {
2285 if c > c1 {
2286 text.push('\t');
2287 }
2288 text.push_str(&self.data.columns[c].name);
2289 }
2290 text.push('\n');
2291 }
2292 for dr in r1..=r2 {
2293 let Some(row_idx) = self.resident_row_for_display(dr) else {
2294 continue;
2295 };
2296 for c in c1..=c2 {
2297 if c > c1 {
2298 text.push('\t');
2299 }
2300 let cell = &self.data.rows[row_idx][c];
2301 let (s, _) = format_cell(cell, &self.resolved_formats[c]);
2302 text.push_str(&s);
2303 }
2304 text.push('\n');
2305 }
2306 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
2307 }
2308
2309 pub fn page_up(&mut self) {
2310 let vh: f32 = self.bounds.size.height.into();
2311 let rows = ((vh - self.header_height) / self.row_height) as i32;
2312 self.move_selection(0, -rows);
2313 }
2314
2315 pub fn page_down(&mut self) {
2316 let vh: f32 = self.bounds.size.height.into();
2317 let rows = ((vh - self.header_height) / self.row_height) as i32;
2318 self.move_selection(0, rows);
2319 }
2320
2321 pub fn handle_key(&mut self, keystroke: &Keystroke) {
2322 if self.filter_panel.is_some() {
2323 match keystroke.key.as_str() {
2324 "escape" => {
2325 self.filter_panel = None;
2326 return;
2327 }
2328 "enter" => {
2329 self.apply_filter_panel();
2330 return;
2331 }
2332 _ => {}
2333 }
2334 let mut edited = false;
2335 if let Some(panel) = &mut self.filter_panel {
2336 let input = panel.active_input_mut();
2337 match keystroke.key.as_str() {
2338 "backspace" => {
2339 input.backspace();
2340 edited = true;
2341 }
2342 "left" => input.move_left(),
2343 "right" => input.move_right(),
2344 _ => {
2345 if let Some(ch) = keystroke_to_char(keystroke) {
2346 input.insert_char(ch);
2347 edited = true;
2348 }
2349 }
2350 }
2351 }
2352 if edited {
2355 self.maybe_auto_apply();
2356 }
2357 return;
2358 }
2359 if self.context_menu.is_some() {
2360 if keystroke.key.as_str() == "escape" {
2361 self.context_menu = None;
2362 }
2363 return;
2364 }
2365 let shift = keystroke.modifiers.shift;
2366 if keystroke.key.as_str() == "escape" {
2367 self.selection = Selection::None;
2368 self.range_anchor = None;
2369 self.range_active = None;
2370 return;
2371 }
2372 let ncols = self.data.columns.len() as i32;
2378 let page = {
2379 let vh: f32 = self.bounds.size.height.into();
2380 (((vh - self.header_height) / self.row_height) as i32 - 1).max(1)
2381 };
2382 let step = match keystroke.key.as_str() {
2383 "up" => Some((0, -1)),
2384 "down" => Some((0, 1)),
2385 "left" => Some((-1, 0)),
2386 "right" => Some((1, 0)),
2387 "home" => Some((-ncols, 0)),
2388 "end" => Some((ncols, 0)),
2389 "pageup" => Some((0, -page)),
2390 "pagedown" => Some((0, page)),
2391 _ => None,
2392 };
2393 if let Some((dx, dy)) = step {
2394 if shift {
2395 self.extend_selection(dx, dy);
2396 } else {
2397 self.move_selection(dx, dy);
2398 }
2399 self.reveal_active_cell();
2400 }
2401 }
2402
2403 fn move_selection(&mut self, dx: i32, dy: i32) {
2404 let nrows = self.display_row_count() as i32;
2405 let ncols = self.data.columns.len() as i32;
2406 if nrows == 0 || ncols == 0 {
2407 return;
2408 }
2409 let last_col = ncols - 1;
2410 match self.selection {
2411 Selection::Cell(row, col) => {
2412 let nr = self.move_data_row(row, dy);
2413 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
2414 self.selection = Selection::Cell(nr, nc);
2415 self.range_anchor = Some((nr, nc));
2416 self.range_active = Some((nr, nc));
2417 }
2418 Selection::Row(row) if dy != 0 => {
2419 let nr = self.move_data_row(row, dy);
2420 self.selection = Selection::Row(nr);
2421 }
2422 Selection::Column(col) if dx != 0 => {
2423 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
2424 self.selection = Selection::Column(nc);
2425 }
2426 _ => {
2427 if let Some(row) = self.first_data_row() {
2428 self.selection = Selection::Cell(row, 0);
2429 self.range_anchor = Some((row, 0));
2430 self.range_active = Some((row, 0));
2431 }
2432 }
2433 }
2434 }
2435
2436 fn first_data_row(&self) -> Option<usize> {
2437 if self.window.is_some() {
2438 return (self.display_row_count() > 0).then_some(0);
2439 }
2440 self.display_rows
2441 .iter()
2442 .position(|row| matches!(row, GridDisplayRow::Data { .. }))
2443 }
2444
2445 fn is_data_display_row(&self, row: usize) -> bool {
2446 if let Some(window) = self.window {
2447 return row < window.total_rows;
2448 }
2449 matches!(
2450 self.display_rows.get(row),
2451 Some(GridDisplayRow::Data { .. })
2452 )
2453 }
2454
2455 fn move_data_row(&self, row: usize, delta: i32) -> usize {
2456 if delta == 0 || self.grouped_column.is_none() || self.window.is_some() {
2457 let last = self.display_row_count().saturating_sub(1) as i32;
2458 return (row as i32 + delta).clamp(0, last) as usize;
2459 }
2460
2461 let direction = delta.signum();
2462 let mut current = row as i32;
2463 let mut remaining = delta.unsigned_abs();
2464 let last = self.display_row_count().saturating_sub(1) as i32;
2465 while remaining > 0 {
2466 let mut candidate = current;
2467 loop {
2468 let next = candidate + direction;
2469 if next < 0 || next > last {
2470 return current as usize;
2471 }
2472 candidate = next;
2473 if matches!(
2474 self.display_rows.get(candidate as usize),
2475 Some(GridDisplayRow::Data { .. })
2476 ) {
2477 break;
2478 }
2479 }
2480 current = candidate;
2481 remaining -= 1;
2482 }
2483 current as usize
2484 }
2485
2486 fn extend_selection(&mut self, dx: i32, dy: i32) {
2490 let nrows = self.display_row_count() as i32;
2491 let ncols = self.data.columns.len() as i32;
2492 if nrows == 0 || ncols == 0 {
2493 return;
2494 }
2495 let last_col = ncols - 1;
2496
2497 if self.range_anchor.is_none() || self.range_active.is_none() {
2499 match self.selection {
2500 Selection::Cell(r, c) => {
2501 self.range_anchor = Some((r, c));
2502 self.range_active = Some((r, c));
2503 }
2504 Selection::CellRange(r1, c1, r2, c2) => {
2505 self.range_anchor = Some((r1, c1));
2506 self.range_active = Some((r2, c2));
2507 }
2508 _ => {
2509 let Some(row) = self.first_data_row() else {
2510 return;
2511 };
2512 self.range_anchor = Some((row, 0));
2513 self.range_active = Some((row, 0));
2514 self.selection = Selection::Cell(row, 0);
2515 }
2516 }
2517 }
2518
2519 let anchor = self.range_anchor.unwrap_or((0, 0));
2520 let active = self.range_active.unwrap_or(anchor);
2521 let nr = self.move_data_row(active.0, dy);
2522 let nc = (active.1 as i32 + dx).clamp(0, last_col) as usize;
2523 self.range_active = Some((nr, nc));
2524
2525 self.selection = if (nr, nc) == anchor {
2526 Selection::Cell(nr, nc)
2527 } else {
2528 Selection::CellRange(
2529 anchor.0.min(nr),
2530 anchor.1.min(nc),
2531 anchor.0.max(nr),
2532 anchor.1.max(nc),
2533 )
2534 };
2535 }
2536
2537 pub(crate) fn ensure_visible(&mut self, row: Option<usize>, col: Option<usize>) {
2542 let (rw, rh) = self.scrollbar_reserved();
2543 let vw: f32 = self.bounds.size.width.into();
2544 let vh: f32 = self.bounds.size.height.into();
2545 let view_w = (vw - self.row_header_width - rw).max(0.0);
2546 let view_h = (vh - self.header_height - rh).max(0.0);
2547 let (max_x, max_y) = self.max_scroll();
2548 let offset = self.scroll_handle.offset();
2549 let (ox, oy) = (f32::from(offset.x), f32::from(offset.y));
2550 let mut sx = ox;
2551 let mut sy = oy;
2552
2553 if let Some(col) = col {
2554 let left: f32 = self.data.columns[..col.min(self.data.columns.len())]
2555 .iter()
2556 .map(|c| c.width)
2557 .sum();
2558 let width = self.data.columns.get(col).map_or(0.0, |c| c.width);
2559 if left < sx {
2560 sx = left;
2561 } else if left + width > sx + view_w {
2562 sx = left + width - view_w;
2563 }
2564 }
2565 if let Some(row) = row {
2566 let top = row as f32 * self.row_height;
2567 if top < sy {
2568 sy = top;
2569 } else if top + self.row_height > sy + view_h {
2570 sy = top + self.row_height - view_h;
2571 }
2572 }
2573 sx = sx.clamp(0.0, max_x);
2574 sy = sy.clamp(0.0, max_y);
2575 if sx != ox || sy != oy {
2576 self.scroll_handle.set_offset(Point {
2577 x: px(sx),
2578 y: px(sy),
2579 });
2580 }
2581 }
2582
2583 fn reveal_active_cell(&mut self) {
2587 let (row, col) = if let Some((r, c)) = self.range_active {
2588 (Some(r), Some(c))
2589 } else {
2590 match self.selection {
2591 Selection::Cell(r, c) => (Some(r), Some(c)),
2592 Selection::Row(r) => (Some(r), None),
2593 Selection::Column(c) => (None, Some(c)),
2594 _ => return,
2595 }
2596 };
2597 self.ensure_visible(row, col);
2598 }
2599
2600 pub(crate) fn hit_test(&self, pos: Point<Pixels>) -> HitResult {
2601 let bounds = self.bounds;
2602 let (sx, sy) = (
2603 f32::from(self.scroll_handle.offset().x),
2604 f32::from(self.scroll_handle.offset().y),
2605 );
2606 let bw: f32 = bounds.size.width.into();
2607 let bh: f32 = bounds.size.height.into();
2608 let (mx, my) = self.max_scroll();
2609 if let Some(menu) = &self.context_menu {
2610 let cw = self.char_width;
2611 let x_rel = f32::from(pos.x);
2614 let y_rel = f32::from(pos.y);
2615 if let Some(idx) = menu_mod::hover_at(menu, x_rel, y_rel, cw) {
2616 return HitResult::ContextMenuItem(idx);
2617 }
2618 }
2619 if my > 0.0
2620 && f32::from(pos.x) >= bw - SCROLLBAR_SIZE
2621 && f32::from(pos.y) >= self.header_height
2622 {
2623 return HitResult::VerticalScrollbar;
2624 }
2625 if mx > 0.0
2626 && f32::from(pos.y) >= bh - SCROLLBAR_SIZE
2627 && f32::from(pos.x) >= self.row_header_width
2628 {
2629 return HitResult::HorizontalScrollbar;
2630 }
2631 let px = f32::from(pos.x);
2637 let py = f32::from(pos.y);
2638 if px < 0.0 || py < 0.0 || px > bw || py > bh {
2639 return HitResult::None;
2640 }
2641 self.hit_test_content(px, py, sx, sy)
2642 }
2643
2644 fn hit_test_content(&self, x: f32, y: f32, sx: f32, sy: f32) -> HitResult {
2645 if y < self.header_height {
2646 if x < self.row_header_width {
2647 return HitResult::Corner;
2648 }
2649 let col_x = x - self.row_header_width + sx;
2650 let mut acc = 0.0;
2651 for (i, col) in self.data.columns.iter().enumerate() {
2652 let right = acc + col.width;
2653 if i + 1 < self.data.columns.len() && col_x >= right - 5.0 && col_x <= right + 5.0 {
2654 return HitResult::ColumnBorder(i);
2655 }
2656 if col_x >= acc && col_x < right {
2657 if col_x >= right - 20.0 {
2658 return HitResult::SortButton(i);
2659 }
2660 return HitResult::ColumnHeader(i);
2661 }
2662 acc = right;
2663 }
2664 return HitResult::None;
2665 }
2666 if x < self.row_header_width {
2667 let row_y = y - self.header_height + sy;
2668 if row_y < 0.0 {
2669 return HitResult::None;
2670 }
2671 let row_idx = (row_y / self.row_height) as usize;
2672 if row_idx < self.display_row_count() {
2673 if let Some(GridDisplayRow::GroupHeader { group }) = self.display_rows.get(row_idx)
2674 {
2675 return HitResult::GroupHeader(*group);
2676 }
2677 return HitResult::RowHeader(row_idx);
2678 }
2679 return HitResult::None;
2680 }
2681 let col_x = x - self.row_header_width + sx;
2682 let row_y = y - self.header_height + sy;
2683 if row_y < 0.0 {
2684 return HitResult::None;
2685 }
2686 let row_idx = (row_y / self.row_height) as usize;
2687 if row_idx >= self.display_row_count() {
2688 return HitResult::None;
2689 }
2690 if let Some(GridDisplayRow::GroupHeader { group }) = self.display_rows.get(row_idx) {
2691 return HitResult::GroupHeader(*group);
2692 }
2693 let mut acc = 0.0;
2694 for (i, col) in self.data.columns.iter().enumerate() {
2695 if col_x >= acc && col_x < acc + col.width {
2696 return HitResult::Cell(row_idx, i);
2697 }
2698 acc += col.width;
2699 }
2700 HitResult::None
2701 }
2702
2703 #[must_use]
2704 pub fn wants_edge_scroll_tick(&self) -> bool {
2705 self.is_dragging
2706 }
2707}
2708
2709fn keystroke_to_char(k: &Keystroke) -> Option<char> {
2710 if k.modifiers.control || k.modifiers.platform || k.modifiers.alt {
2711 return None;
2712 }
2713 if let Some(key_char) = k.key_char.as_ref() {
2714 return key_char.chars().next();
2715 }
2716 if k.key.chars().count() == 1 {
2717 let c = k.key.chars().next()?;
2718 if k.modifiers.shift {
2719 Some(c.to_ascii_uppercase())
2720 } else {
2721 Some(c)
2722 }
2723 } else {
2724 None
2725 }
2726}
2727
2728#[cfg(test)]
2729#[allow(
2730 clippy::unwrap_used,
2731 clippy::expect_used,
2732 clippy::field_reassign_with_default
2733)]
2734mod tests {
2735 use super::*;
2736 use crate::data::{CellValue, Column, ColumnKind};
2737 use crate::grid::state::state_inner::{edge_scroll_speed, format_current_status};
2738
2739 fn input_with(text: &str, cursor: usize) -> TextInput {
2740 let mut p = TextInput::new(text.to_owned());
2741 p.cursor_chars = cursor;
2742 p
2743 }
2744
2745 #[test]
2746 fn text_input_new_cursors_at_char_count_not_bytes() {
2747 let p = TextInput::new("hé🙂".into());
2749 assert_eq!(p.cursor_chars, 3);
2750 assert_eq!(p.value.len(), 7);
2751 }
2752
2753 #[test]
2754 fn text_input_insert_emoji_at_start_does_not_panic() {
2755 let mut p = input_with("ab", 0);
2756 p.insert_char('\u{1F600}');
2757 assert_eq!(p.value, "\u{1F600}ab");
2758 assert_eq!(p.cursor_chars, 1);
2759 }
2760
2761 #[test]
2762 fn text_input_insert_in_middle_keeps_cursor_at_char_position() {
2763 let mut p = input_with("helloworld", 5);
2764 p.insert_char(' ');
2765 assert_eq!(p.value, "hello world");
2766 assert_eq!(p.cursor_chars, 6);
2767 }
2768
2769 #[test]
2770 fn text_input_backspace_at_zero_is_noop() {
2771 let mut p = input_with("abc", 0);
2772 p.backspace();
2773 assert_eq!(p.value, "abc");
2774 assert_eq!(p.cursor_chars, 0);
2775 }
2776
2777 #[test]
2778 fn text_input_backspace_removes_one_char_value() {
2779 let mut p = input_with("héx", 2);
2781 p.backspace();
2782 assert_eq!(p.value, "hx");
2783 assert_eq!(p.cursor_chars, 1);
2784 }
2785
2786 #[test]
2787 fn text_input_clamp_cursor_pulls_back_past_end() {
2788 let mut p = input_with("abc", 99);
2789 p.clamp_cursor();
2790 assert_eq!(p.cursor_chars, 3);
2791 }
2792
2793 #[test]
2794 fn text_input_move_left_and_right_respect_bounds() {
2795 let mut p = input_with("ab", 2);
2796 p.move_right();
2797 assert_eq!(p.cursor_chars, 2);
2798 p.move_left();
2799 p.move_left();
2800 p.move_left();
2801 assert_eq!(p.cursor_chars, 0);
2802 }
2803
2804 #[test]
2805 fn edge_scroll_speed_stops_outside_band() {
2806 assert_eq!(edge_scroll_speed(120.0), 0.0);
2808 assert_eq!(edge_scroll_speed(90.01), 0.0);
2809 assert_eq!(edge_scroll_speed(90.0), 4.0);
2811 assert_eq!(edge_scroll_speed(60.0), 4.0);
2812 assert_eq!(edge_scroll_speed(59.99), 8.0);
2813 assert_eq!(edge_scroll_speed(30.0), 8.0);
2815 assert_eq!(edge_scroll_speed(29.99), 16.0);
2816 assert_eq!(edge_scroll_speed(0.0), 16.0);
2818 assert_eq!(edge_scroll_speed(29.99), 16.0);
2819 }
2820
2821 #[test]
2822 fn edge_scroll_speed_caps_negative_runaway() {
2823 assert_eq!(edge_scroll_speed(-100.0), 16.0);
2825 assert_eq!(edge_scroll_speed(-1000.0), 16.0);
2826 }
2827
2828 #[allow(clippy::expect_used, clippy::unwrap_used)]
2836 #[test]
2837 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2838 fn grid_state_behavior_under_application() {
2839 gpui::Application::new().run(|cx| {
2840 let focus = cx.focus_handle();
2841
2842 let mut state = GridState::new(
2844 GridData::new(
2845 vec![Column::new("n", ColumnKind::Integer, 100.0)],
2846 vec![vec![CellValue::Integer(1)]],
2847 )
2848 .expect("rectangular"),
2849 crate::config::GridConfig::default(),
2850 focus.clone(),
2851 );
2852 let _ = format_current_status(&state);
2853 assert_eq!(state.selection, Selection::None);
2854
2855 state.last_mouse_pos = Some(Point {
2857 x: px(120.0),
2858 y: px(80.0),
2859 });
2860 let s = format_current_status(&state);
2861 assert!(s.contains("(120, 80)"), "missing positional, got: {s}");
2862
2863 let mut state = GridState::new(
2865 GridData::new(
2866 vec![Column::new("name", ColumnKind::Text, 100.0)],
2867 vec![
2868 vec![CellValue::Text("alpha".into())],
2869 vec![CellValue::Text("beeb".into())],
2870 vec![CellValue::Text("gamma".into())],
2871 ],
2872 )
2873 .expect("rectangular"),
2874 crate::config::GridConfig::default(),
2875 focus.clone(),
2876 );
2877 state.filters[0] = ColumnFilter {
2878 predicate: FilterPredicate::Text {
2879 op: TextOp::Contains,
2880 operand: "a".into(),
2881 },
2882 values: None,
2883 };
2884 state.toggle_sort(0);
2885 state.recompute();
2886 assert_eq!(state.display_indices.as_slice(), &[0, 2]);
2887 state.toggle_sort(0);
2888 state.recompute();
2889 assert_eq!(state.display_indices.as_slice(), &[2, 0]);
2890 state.filters[0] = ColumnFilter::default();
2891 state.toggle_sort(0);
2892 state.recompute();
2893 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
2894
2895 let mut state = GridState::new(
2897 GridData::new(
2898 vec![Column::new("v", ColumnKind::Integer, 80.0)],
2899 vec![vec![CellValue::Integer(1)]],
2900 )
2901 .expect("rectangular"),
2902 crate::config::GridConfig::default(),
2903 focus.clone(),
2904 );
2905 state.toggle_sort(0);
2906 assert_eq!(state.sort, Some((0, SortDirection::Ascending)));
2907 state.toggle_sort(0);
2908 assert_eq!(state.sort, Some((0, SortDirection::Descending)));
2909 state.toggle_sort(0);
2910 assert_eq!(state.sort, None);
2911
2912 let mut state = GridState::new(
2914 GridData::new(
2915 vec![
2916 Column::new("a", ColumnKind::Integer, 80.0),
2917 Column::new("b", ColumnKind::Integer, 80.0),
2918 ],
2919 vec![vec![CellValue::Integer(1), CellValue::Integer(2)]],
2920 )
2921 .expect("rectangular"),
2922 crate::config::GridConfig::default(),
2923 focus.clone(),
2924 );
2925 state.select_all();
2926 assert_eq!(state.selection, Selection::CellRange(0, 0, 0, 1));
2927
2928 let mut state = GridState::new(
2930 GridData::new(vec![Column::new("a", ColumnKind::Integer, 80.0)], vec![])
2931 .expect("rectangular"),
2932 crate::config::GridConfig::default(),
2933 focus.clone(),
2934 );
2935 state.select_all();
2936 assert_eq!(state.selection, Selection::None);
2937
2938 let mut state = GridState::new(
2940 GridData::new(
2941 vec![Column::new("v", ColumnKind::Decimal, 100.0)],
2942 vec![vec![CellValue::Decimal(1.234)]],
2943 )
2944 .expect("rectangular"),
2945 crate::config::GridConfig::default(),
2946 focus.clone(),
2947 );
2948 assert_eq!(state.resolved_formats[0].number.decimals, 2);
2949 let mut cfg = crate::config::GridConfig::default();
2950 cfg.column_overrides = vec![crate::config::ColumnOverride {
2951 number: Some(crate::config::NumberFormat {
2952 decimals: 6,
2953 ..Default::default()
2954 }),
2955 ..Default::default()
2956 }];
2957 state.set_config(cfg);
2958 assert_eq!(state.resolved_formats[0].number.decimals, 6);
2959
2960 let mut state = GridState::new(
2962 GridData::new(
2963 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2964 vec![vec![CellValue::Integer(1)]],
2965 )
2966 .expect("rectangular"),
2967 crate::config::GridConfig::default(),
2968 focus.clone(),
2969 );
2970 assert!(!state.wants_edge_scroll_tick());
2971 state.is_dragging = true;
2972 assert!(state.wants_edge_scroll_tick());
2973
2974 cx.quit();
2975 });
2976 }
2977
2978 #[allow(clippy::expect_used, clippy::unwrap_used)]
2979 #[test]
2980 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2981 fn context_menu_request_construction() {
2982 use crate::grid::context_menu::ContextMenuTarget;
2983
2984 gpui::Application::new().run(|cx| {
2985 let focus = cx.focus_handle();
2986
2987 let mut state = GridState::new(
2989 GridData::new(
2990 vec![
2991 Column::new("id", ColumnKind::Integer, 80.0),
2992 Column::new("name", ColumnKind::Text, 100.0),
2993 ],
2994 vec![
2995 vec![CellValue::Integer(1), CellValue::Text("alpha".into())],
2996 vec![CellValue::Integer(2), CellValue::Text("beta".into())],
2997 vec![CellValue::Integer(3), CellValue::Text("gamma".into())],
2998 ],
2999 )
3000 .expect("rectangular"),
3001 crate::config::GridConfig::default(),
3002 focus.clone(),
3003 );
3004 state.sort = Some((0, SortDirection::Descending));
3006 state.recompute();
3007 assert_eq!(state.display_indices.as_slice(), &[2, 1, 0]);
3008
3009 let target = ContextMenuTarget::Cell {
3011 display_row_index: 0,
3012 source_row_index: 2,
3013 column_index: 1,
3014 };
3015 let sel = Selection::Cell(0, 1);
3016 let req = state.build_context_menu_request(target, &sel);
3017 assert_eq!(req.target.column_index(), Some(1));
3018 let cells = req.selected_cells();
3019 assert_eq!(cells.len(), 1);
3020 assert_eq!(cells[0].source_row_index, 2);
3021 assert_eq!(cells[0].column_name, "name");
3022 assert_eq!(cells[0].value, CellValue::Text("gamma".into()));
3023 let rows = req.selected_rows();
3024 assert_eq!(rows.len(), 1);
3025 assert_eq!(rows[0].source_row_index, 2);
3026 assert_eq!(rows[0].value_by_name("id"), Some(&CellValue::Integer(3)));
3027
3028 let target = ContextMenuTarget::Cell {
3030 display_row_index: 0,
3031 source_row_index: 2,
3032 column_index: 0,
3033 };
3034 let sel = Selection::CellRange(0, 0, 1, 1);
3035 let req = state.build_context_menu_request(target, &sel);
3036 assert_eq!(req.selected_cell_count(), 4); let rows = req.selected_rows();
3038 assert_eq!(rows.len(), 2);
3039 assert_eq!(rows[0].source_row_index, 2);
3041 assert_eq!(rows[1].source_row_index, 1);
3042
3043 let target = ContextMenuTarget::RowHeader {
3045 display_row_index: 1,
3046 source_row_index: 1,
3047 };
3048 let sel = Selection::RowRange(0, 2);
3049 let req = state.build_context_menu_request(target, &sel);
3050 let rows = req.selected_rows();
3051 assert_eq!(rows.len(), 3);
3052 assert_eq!(rows[0].values.len(), 2);
3054 assert_eq!(req.selected_cell_count(), 6); let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
3060 let sel = Selection::Column(0);
3061 let req = state.build_context_menu_request(target, &sel);
3062 assert!(req.is_column_oriented());
3063 assert_eq!(req.selected_row_count(), 0);
3064 assert!(req.selected_rows().is_empty());
3065 assert_eq!(req.selected_cells().len(), 3); let empty_state = GridState::new(
3069 GridData::new(vec![Column::new("x", ColumnKind::Integer, 80.0)], vec![])
3070 .expect("rectangular"),
3071 crate::config::GridConfig::default(),
3072 focus.clone(),
3073 );
3074 let target = ContextMenuTarget::Cell {
3075 display_row_index: 0,
3076 source_row_index: 0,
3077 column_index: 0,
3078 };
3079 let req = empty_state.build_context_menu_request(target, &Selection::None);
3080 assert!(req.selected_cells().is_empty());
3081 assert!(req.selected_rows().is_empty());
3082
3083 cx.quit();
3084 });
3085 }
3086
3087 #[allow(clippy::expect_used, clippy::unwrap_used)]
3088 #[test]
3089 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3090 fn effective_selection_for_context_target() {
3091 gpui::Application::new().run(|cx| {
3092 let focus = cx.focus_handle();
3093 let mut state = GridState::new(
3094 GridData::new(
3095 vec![
3096 Column::new("a", ColumnKind::Integer, 80.0),
3097 Column::new("b", ColumnKind::Integer, 80.0),
3098 ],
3099 vec![
3100 vec![CellValue::Integer(1), CellValue::Integer(2)],
3101 vec![CellValue::Integer(3), CellValue::Integer(4)],
3102 ],
3103 )
3104 .expect("rectangular"),
3105 crate::config::GridConfig::default(),
3106 focus,
3107 );
3108
3109 state.selection = Selection::Cell(0, 0);
3111 let target = ContextMenuTarget::Cell {
3112 display_row_index: 1,
3113 source_row_index: 1,
3114 column_index: 1,
3115 };
3116 let eff = state.effective_selection_for_context_target(&target);
3117 assert_eq!(eff, Selection::Cell(1, 1));
3118
3119 state.selection = Selection::CellRange(0, 0, 1, 1);
3121 let target = ContextMenuTarget::Cell {
3122 display_row_index: 1,
3123 source_row_index: 1,
3124 column_index: 1,
3125 };
3126 let eff = state.effective_selection_for_context_target(&target);
3127 assert_eq!(eff, Selection::CellRange(0, 0, 1, 1));
3128
3129 state.selection = Selection::Cell(0, 0);
3131 let target = ContextMenuTarget::RowHeader {
3132 display_row_index: 1,
3133 source_row_index: 1,
3134 };
3135 let eff = state.effective_selection_for_context_target(&target);
3136 assert_eq!(eff, Selection::Row(1));
3137
3138 state.selection = Selection::RowRange(0, 1);
3140 let target = ContextMenuTarget::RowHeader {
3141 display_row_index: 1,
3142 source_row_index: 1,
3143 };
3144 let eff = state.effective_selection_for_context_target(&target);
3145 assert_eq!(eff, Selection::RowRange(0, 1));
3146
3147 state.selection = Selection::Cell(1, 1);
3149 let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
3150 let eff = state.effective_selection_for_context_target(&target);
3151 assert_eq!(eff, Selection::Cell(1, 1));
3152
3153 cx.quit();
3154 });
3155 }
3156
3157 #[allow(clippy::expect_used, clippy::unwrap_used)]
3158 #[test]
3159 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3160 fn context_menu_target_from_hit_maps_correctly() {
3161 gpui::Application::new().run(|cx| {
3162 let focus = cx.focus_handle();
3163 let state = GridState::new(
3164 GridData::new(
3165 vec![Column::new("a", ColumnKind::Integer, 80.0)],
3166 vec![vec![CellValue::Integer(1)], vec![CellValue::Integer(2)]],
3167 )
3168 .expect("rectangular"),
3169 crate::config::GridConfig::default(),
3170 focus,
3171 );
3172
3173 let t = state
3175 .context_menu_target_from_hit(HitResult::Cell(1, 0))
3176 .unwrap();
3177 assert_eq!(
3178 t,
3179 ContextMenuTarget::Cell {
3180 display_row_index: 1,
3181 source_row_index: 1,
3182 column_index: 0,
3183 }
3184 );
3185
3186 let t = state
3188 .context_menu_target_from_hit(HitResult::RowHeader(0))
3189 .unwrap();
3190 assert_eq!(
3191 t,
3192 ContextMenuTarget::RowHeader {
3193 display_row_index: 0,
3194 source_row_index: 0,
3195 }
3196 );
3197
3198 let t = state
3200 .context_menu_target_from_hit(HitResult::ColumnHeader(0))
3201 .unwrap();
3202 assert_eq!(t, ContextMenuTarget::ColumnHeader { column_index: 0 });
3203
3204 let t = state
3206 .context_menu_target_from_hit(HitResult::SortButton(0))
3207 .unwrap();
3208 assert_eq!(t, ContextMenuTarget::SortButton { column_index: 0 });
3209
3210 assert!(state
3212 .context_menu_target_from_hit(HitResult::VerticalScrollbar)
3213 .is_none());
3214 assert!(state
3215 .context_menu_target_from_hit(HitResult::None)
3216 .is_none());
3217
3218 cx.quit();
3219 });
3220 }
3221
3222 #[allow(clippy::expect_used, clippy::unwrap_used)]
3223 #[test]
3224 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3225 fn convert_context_menu_items_maps_variants() {
3226 use crate::grid::context_menu::ContextMenuItem;
3227
3228 let items = vec![
3229 ContextMenuItem::BuiltIn(MenuAction::SortAscending),
3230 ContextMenuItem::action("copy", "Copy value"),
3231 ContextMenuItem::separator(),
3232 ];
3233 let internal = GridState::convert_context_menu_items(items);
3234 assert!(matches!(
3235 internal[0],
3236 MenuItem::Action(MenuAction::SortAscending)
3237 ));
3238 assert!(
3239 matches!(&internal[1], MenuItem::Custom { id, label } if id == "copy" && label == "Copy value")
3240 );
3241 assert!(matches!(internal[2], MenuItem::Separator));
3242 }
3243
3244 #[allow(clippy::expect_used, clippy::unwrap_used)]
3245 #[test]
3246 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3247 fn execute_custom_context_menu_action_invokes_provider() {
3248 use crate::grid::context_menu::{
3249 ContextMenuProvider, ContextMenuProviderHandle, ContextMenuRequest,
3250 };
3251 use std::sync::{Arc, Mutex};
3252
3253 #[derive(Default)]
3254 struct TestProvider {
3255 last_action: Arc<Mutex<Option<String>>>,
3256 }
3257 impl ContextMenuProvider for TestProvider {
3258 fn menu_items(&self, _request: &ContextMenuRequest) -> Vec<ContextMenuItem> {
3259 vec![ContextMenuItem::action("test", "Test")]
3260 }
3261 fn on_action(
3262 &self,
3263 action_id: &str,
3264 _request: &ContextMenuRequest,
3265 _state: &mut GridState,
3266 _cx: &mut gpui::App,
3267 ) {
3268 *self.last_action.lock().unwrap() = Some(action_id.to_string());
3269 }
3270 }
3271
3272 gpui::Application::new().run(|cx| {
3273 let focus = cx.focus_handle();
3274 let mut state = GridState::new(
3275 GridData::new(
3276 vec![Column::new("a", ColumnKind::Integer, 80.0)],
3277 vec![vec![CellValue::Integer(1)]],
3278 )
3279 .expect("rectangular"),
3280 crate::config::GridConfig::default(),
3281 focus,
3282 );
3283
3284 let last = Arc::new(Mutex::new(None));
3285 state.context_menu_provider = Some(ContextMenuProviderHandle::new(TestProvider {
3286 last_action: last.clone(),
3287 }));
3288
3289 let target = ContextMenuTarget::Cell {
3290 display_row_index: 0,
3291 source_row_index: 0,
3292 column_index: 0,
3293 };
3294 let request = state.build_context_menu_request(target, &Selection::Cell(0, 0));
3295 state.execute_custom_context_menu_action(
3296 PendingCustomContextMenuAction {
3297 id: "test".into(),
3298 request,
3299 },
3300 cx,
3301 );
3302 assert_eq!(*last.lock().unwrap(), Some("test".to_string()));
3303 assert!(state.context_menu.is_none());
3304
3305 cx.quit();
3306 });
3307 }
3308
3309 #[test]
3310 fn filter_panel_to_filter_with_all_checked_has_no_value_set() {
3311 let panel = FilterPanel {
3312 col: 0,
3313 anchor: Point {
3314 x: px(0.0),
3315 y: px(0.0),
3316 },
3317 kind: ColumnKind::Text,
3318 search: TextInput::default(),
3319 op_index: 0,
3320 op_menu_open: false,
3321 operand_a: TextInput::default(),
3322 operand_b: TextInput::default(),
3323 focus: FilterInput::Search,
3324 auto_apply: true,
3325 distinct: vec![
3326 FilterValueRow {
3327 label: "alpha".into(),
3328 checked: true,
3329 },
3330 FilterValueRow {
3331 label: "beta".into(),
3332 checked: true,
3333 },
3334 ],
3335 };
3336 let f = panel.to_filter();
3337 assert!(f.values.is_none(), "all checked => no value allow-list");
3338 assert!(
3339 !f.is_active(),
3340 "default predicate + all checked => inactive"
3341 );
3342 }
3343
3344 #[test]
3345 fn filter_panel_to_filter_with_unchecked_value_builds_allow_set() {
3346 let panel = FilterPanel {
3347 col: 0,
3348 anchor: Point {
3349 x: px(0.0),
3350 y: px(0.0),
3351 },
3352 kind: ColumnKind::Text,
3353 search: TextInput::default(),
3354 op_index: 0,
3355 op_menu_open: false,
3356 operand_a: TextInput::default(),
3357 operand_b: TextInput::default(),
3358 focus: FilterInput::Search,
3359 auto_apply: true,
3360 distinct: vec![
3361 FilterValueRow {
3362 label: "alpha".into(),
3363 checked: true,
3364 },
3365 FilterValueRow {
3366 label: "beta".into(),
3367 checked: false,
3368 },
3369 ],
3370 };
3371 let f = panel.to_filter();
3372 assert!(f.is_active(), "unchecked value => active filter");
3373 let set = f.values.expect("should have a value set");
3374 assert!(set.contains("alpha"));
3375 assert!(!set.contains("beta"));
3376 }
3377
3378 #[test]
3379 fn filter_panel_visible_indices_respects_search() {
3380 let panel = FilterPanel {
3381 col: 0,
3382 anchor: Point {
3383 x: px(0.0),
3384 y: px(0.0),
3385 },
3386 kind: ColumnKind::Text,
3387 search: TextInput::new("al".into()),
3388 op_index: 0,
3389 op_menu_open: false,
3390 operand_a: TextInput::default(),
3391 operand_b: TextInput::default(),
3392 focus: FilterInput::Search,
3393 auto_apply: true,
3394 distinct: vec![
3395 FilterValueRow {
3396 label: "alpha".into(),
3397 checked: true,
3398 },
3399 FilterValueRow {
3400 label: "beta".into(),
3401 checked: true,
3402 },
3403 FilterValueRow {
3404 label: "gamma".into(),
3405 checked: true,
3406 },
3407 ],
3408 };
3409 let vis = panel.visible_indices();
3410 assert_eq!(vis, vec![0], "search 'al' matches only alpha");
3411 }
3412
3413 #[test]
3414 fn filter_panel_all_checked_ignores_search() {
3415 let mut panel = FilterPanel {
3416 col: 0,
3417 anchor: Point {
3418 x: px(0.0),
3419 y: px(0.0),
3420 },
3421 kind: ColumnKind::Text,
3422 search: TextInput::new("al".into()),
3423 op_index: 0,
3424 op_menu_open: false,
3425 operand_a: TextInput::default(),
3426 operand_b: TextInput::default(),
3427 focus: FilterInput::Search,
3428 auto_apply: true,
3429 distinct: vec![
3430 FilterValueRow {
3431 label: "alpha".into(),
3432 checked: true,
3433 },
3434 FilterValueRow {
3435 label: "beta".into(),
3436 checked: false,
3437 },
3438 FilterValueRow {
3439 label: "gamma".into(),
3440 checked: true,
3441 },
3442 ],
3443 };
3444 assert!(
3447 !panel.all_checked(),
3448 "beta is unchecked, so not all values are checked (search is irrelevant)"
3449 );
3450
3451 panel.search = TextInput::new("zzz".into());
3453 for row in &mut panel.distinct {
3454 row.checked = true;
3455 }
3456 assert!(
3457 panel.all_checked(),
3458 "all values checked -> Select All stays checked regardless of empty search"
3459 );
3460 }
3461
3462 #[allow(clippy::expect_used, clippy::unwrap_used)]
3463 #[test]
3464 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3465 fn filter_panel_open_apply_clear_state_flow() {
3466 gpui::Application::new().run(|cx| {
3467 let focus = cx.focus_handle();
3468 let mut state = GridState::new(
3469 GridData::new(
3470 vec![Column::new("name", ColumnKind::Text, 100.0)],
3471 vec![
3472 vec![CellValue::Text("alpha".into())],
3473 vec![CellValue::Text("beta".into())],
3474 vec![CellValue::Text("gamma".into())],
3475 ],
3476 )
3477 .expect("rectangular"),
3478 crate::config::GridConfig::default(),
3479 focus,
3480 );
3481
3482 let anchor = Point {
3484 x: px(50.0),
3485 y: px(20.0),
3486 };
3487 state.open_filter_panel(0, Some(anchor));
3488 let panel = state.filter_panel.as_ref().expect("panel should be open");
3489 assert_eq!(panel.col, 0);
3490 assert_eq!(panel.anchor, anchor);
3491 assert_eq!(panel.distinct.len(), 3);
3492 assert!(
3493 panel.distinct.iter().all(|r| r.checked),
3494 "all checked by default"
3495 );
3496 assert!(panel.auto_apply, "auto_apply defaults to true");
3497 assert_eq!(panel.kind, ColumnKind::Text);
3498
3499 state.toggle_filter_value(1);
3501 state.apply_filter_panel();
3502 assert_eq!(
3503 state.display_indices.as_slice(),
3504 &[0, 2],
3505 "beta should be filtered out"
3506 );
3507
3508 state.clear_filter_panel();
3510 assert_eq!(
3511 state.display_indices.as_slice(),
3512 &[0, 1, 2],
3513 "all rows visible after clear"
3514 );
3515 assert!(
3516 state.filters[0] == ColumnFilter::default(),
3517 "filter reset to default"
3518 );
3519
3520 state.open_filter_panel(0, Some(anchor));
3522 let panel = state.filter_panel.as_mut().expect("panel open");
3523 panel.op_index = 1; panel.operand_a = TextInput::new("a".into());
3525 state.apply_filter_panel();
3526 assert_eq!(
3527 state.display_indices.as_slice(),
3528 &[0, 2],
3529 "contains 'a' matches alpha and gamma"
3530 );
3531
3532 state.clear_filter_panel();
3534 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
3535
3536 cx.quit();
3537 });
3538 }
3539}