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