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