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::sync::Arc;
20
21use crate::grid::menu as menu_mod;
23#[allow(unused_imports)]
24pub(crate) use crate::grid::menu::{ContextMenu, MenuAction, MenuItem};
25use crate::grid::selection::{
26 is_cell_selected, is_row_selected, HitResult, ScrollbarAxis, Selection, SortDirection,
27};
28
29use crate::grid::context_menu::{
30 ColumnContext, ContextMenuItem, ContextMenuProviderHandle, ContextMenuRequest,
31 ContextMenuSelection, ContextMenuTarget, PendingCustomContextMenuAction,
32};
33
34pub mod state_inner {
38 use super::{
39 format_cell, CellValue, GridState, HitResult, Pixels, Point, ResolvedColumnFormat,
40 };
41 pub use crate::grid::selection::screen_to_content;
42 pub use crate::grid::selection::to_grid_relative;
43 use std::fmt::Write as _;
44
45 const REALLY_FAST: f32 = 16.0;
59 pub fn edge_scroll_speed(dist_from_edge: f32) -> f32 {
60 if dist_from_edge > 90.0 {
61 return 0.0;
62 }
63 if dist_from_edge < 0.0 {
64 return REALLY_FAST;
67 }
68 if dist_from_edge < 30.0 {
69 REALLY_FAST
70 } else if dist_from_edge < 60.0 {
71 8.0
72 } else {
73 4.0
74 }
75 }
76
77 pub fn apply_edge_scroll(state: &mut GridState) -> bool {
78 if !state.is_dragging {
79 return false;
80 }
81 let Some(pos) = state.last_mouse_pos else {
82 return false;
83 };
84 let bounds = state.bounds;
85 let vw: f32 = bounds.size.width.into();
94 let vh: f32 = bounds.size.height.into();
95 let px: f32 = pos.x.into();
96 let py: f32 = pos.y.into();
97 let right_dist = vw - px;
98 let left_dist = px - state.row_header_width;
99 let bottom_dist = vh - py;
100 let top_dist = py - state.header_height;
101 let mut dx = 0.0_f32;
102 let mut dy = 0.0_f32;
103 if right_dist < 90.0 && right_dist <= left_dist {
104 dx = edge_scroll_speed(right_dist);
105 } else if left_dist < 90.0 {
106 dx = -edge_scroll_speed(left_dist);
107 }
108 if bottom_dist < 90.0 && bottom_dist <= top_dist {
109 dy = edge_scroll_speed(bottom_dist);
110 } else if top_dist < 90.0 {
111 dy = -edge_scroll_speed(top_dist);
112 }
113 if dx == 0.0 && dy == 0.0 {
114 return false;
115 }
116 state.scroll_one_edge_tick(dx, dy);
117 if state.drag_start.is_some() {
118 state.update_drag_from_last();
119 }
120 true
121 }
122
123 #[must_use]
124 pub fn format_current_status(state: &GridState) -> String {
125 let scroll = state.scroll_handle.offset();
126 let (click_col, click_row) = col_row_from_hit(state.click_hit);
127 let (hover_col, hover_row) = col_row_from_hit(state.hover_hit);
128 let mut out = String::new();
129 let _ = write!(
130 out,
131 "Click: {} Scroll@Click: {} Cell: {} | Cur: {} Scroll: {} Over: {}",
132 fmt_point(state.click_pos),
133 fmt_point(state.scroll_at_click),
134 fmt_cr(click_col, click_row),
135 fmt_point(state.last_mouse_pos),
136 fmt_point(Some(scroll)),
137 fmt_cr(hover_col, hover_row),
138 );
139 out
140 }
141
142 fn col_row_from_hit(hit: Option<HitResult>) -> (Option<usize>, Option<usize>) {
143 match hit {
144 Some(HitResult::Cell(r, c)) => (Some(c), Some(r)),
145 Some(HitResult::RowHeader(r)) => (None, Some(r)),
146 Some(HitResult::ColumnHeader(c)) | Some(HitResult::SortButton(c)) => (Some(c), None),
147 _ => (None, None),
148 }
149 }
150
151 fn fmt_point(p: Option<Point<Pixels>>) -> String {
152 match p {
153 Some(p) => format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y)),
154 None => "—".into(),
155 }
156 }
157
158 fn fmt_cr(c: Option<usize>, r: Option<usize>) -> String {
159 match (c, r) {
160 (Some(c), Some(r)) => format!("(col {c}, row {r})"),
161 (Some(c), None) => format!("(col {c})"),
162 (None, Some(r)) => format!("(row {r})"),
163 (None, None) => "—".into(),
164 }
165 }
166
167 #[must_use]
168 pub fn cell_text(cell: &CellValue, fmt: &ResolvedColumnFormat) -> String {
169 format_cell(cell, fmt).0
170 }
171}
172
173pub const SCROLLBAR_SIZE: f32 = 20.0;
175pub const EDGE_SCROLL_TICK_MS: u64 = 16;
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct RowWindow {
187 pub total_rows: usize,
189 pub offset: usize,
191}
192
193#[derive(Debug)]
195pub struct GridState {
196 pub data: GridData,
197 pub config: GridConfig,
198 pub window: Option<RowWindow>,
200 pub resolved_formats: Vec<ResolvedColumnFormat>,
204 pub(crate) data_rows: Arc<Vec<Vec<CellValue>>>,
208 pub display_indices: Arc<Vec<usize>>,
209 pub selection: Selection,
210 pub(crate) range_anchor: Option<(usize, usize)>,
214 pub(crate) range_active: Option<(usize, usize)>,
217 pub sort: Option<(usize, SortDirection)>,
218 pub filters: Vec<ColumnFilter>,
219 pub scroll_handle: ScrollHandle,
220 pub focus_handle: FocusHandle,
221 pub bounds: Bounds<Pixels>,
222 pub row_height: f32,
223 pub header_height: f32,
224 pub row_header_width: f32,
225 pub font_size: f32,
226 pub char_width: f32,
227 pub theme: GridTheme,
228 pub is_dragging: bool,
229 pub drag_start: Option<Point<Pixels>>,
230 pub drag_start_hit: Option<HitResult>,
231 pub scroll_at_click: Option<Point<Pixels>>,
232 pub last_mouse_pos: Option<Point<Pixels>>,
233 pub status_bar_height: f32,
234 pub debug_bar_enabled: bool,
239 pub click_pos: Option<Point<Pixels>>,
240 pub click_hit: Option<HitResult>,
241 pub hover_hit: Option<HitResult>,
242 pub resizing_col: Option<usize>,
243 pub resize_start_x: f32,
244 pub resize_start_width: f32,
245 pub context_menu: Option<ContextMenu>,
246 pub filter_panel: Option<FilterPanel>,
247 pub pending_action: Option<(MenuAction, usize)>,
248 pub(crate) pending_custom_context_menu_action: Option<PendingCustomContextMenuAction>,
249 pub(crate) context_menu_provider: Option<ContextMenuProviderHandle>,
250 pub scrollbar_drag: Option<ScrollbarAxis>,
251 pub scrollbar_drag_start_offset: f32,
252 pub scrollbar_drag_start_pos: f32,
253 pub(crate) window_viewport: Size<Pixels>,
257 pub(crate) edge_scroll_active: bool,
261 pub(crate) column_meta: Arc<[ColumnContext]>,
265 pub(crate) self_weak: Option<gpui::WeakEntity<GridState>>,
269 pub(crate) busy: Option<BusyState>,
273}
274
275#[derive(Clone, Debug)]
279pub struct BusyState {
280 pub label: String,
282 pub progress: Option<f32>,
285}
286
287#[derive(Clone, Debug, Default)]
291pub struct TextInput {
292 pub value: String,
294 pub cursor_chars: usize,
296}
297
298impl TextInput {
299 fn new(value: String) -> Self {
300 let cursor_chars = value.chars().count();
301 Self {
302 value,
303 cursor_chars,
304 }
305 }
306
307 fn clamp_cursor(&mut self) {
308 let total = self.value.chars().count();
309 if self.cursor_chars > total {
310 self.cursor_chars = total;
311 }
312 }
313
314 fn insert_char(&mut self, ch: char) {
315 let byte_idx = byte_index_for_char(&self.value, self.cursor_chars);
316 self.value.insert(byte_idx, ch);
317 self.cursor_chars += 1;
318 }
319
320 fn backspace(&mut self) {
321 if self.cursor_chars == 0 {
322 return;
323 }
324 let end = byte_index_for_char(&self.value, self.cursor_chars);
325 let start = byte_index_for_char(&self.value, self.cursor_chars - 1);
326 self.value.replace_range(start..end, "");
327 self.cursor_chars -= 1;
328 }
329
330 fn move_left(&mut self) {
331 if self.cursor_chars > 0 {
332 self.cursor_chars -= 1;
333 }
334 }
335
336 fn move_right(&mut self) {
337 self.clamp_cursor();
338 if self.cursor_chars < self.value.chars().count() {
339 self.cursor_chars += 1;
340 }
341 }
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq)]
346pub enum FilterInput {
347 Search,
349 OperandA,
351 OperandB,
353}
354
355#[derive(Clone, Debug)]
357pub struct FilterValueRow {
358 pub label: String,
360 pub checked: bool,
362}
363
364#[derive(Clone, Debug)]
371pub struct FilterPanel {
372 pub col: usize,
374 pub anchor: Point<Pixels>,
376 pub kind: ColumnKind,
378 pub search: TextInput,
380 pub op_index: usize,
383 pub op_menu_open: bool,
385 pub operand_a: TextInput,
387 pub operand_b: TextInput,
389 pub focus: FilterInput,
391 pub auto_apply: bool,
393 pub distinct: Vec<FilterValueRow>,
395}
396
397const TEXT_OP_LABELS: &[&str] = &[
401 "Choose One",
402 "contains",
403 "does not contain",
404 "begins with",
405 "ends with",
406 "is",
407 "is not",
408 "matches (regex)",
409];
410
411const NUMBER_OP_LABELS: &[&str] = &[
413 "Choose One",
414 "equal to",
415 "not equal to",
416 "greater than",
417 "greater than or equal to",
418 "less than",
419 "less than or equal to",
420 "between",
421 "not between",
422];
423
424impl FilterPanel {
425 #[must_use]
427 pub fn op_labels(&self) -> &'static [&'static str] {
428 if uses_number_ops(self.kind) {
429 NUMBER_OP_LABELS
430 } else {
431 TEXT_OP_LABELS
432 }
433 }
434
435 #[must_use]
437 pub fn current_op_label(&self) -> &'static str {
438 self.op_labels()
439 .get(self.op_index)
440 .copied()
441 .unwrap_or("Choose One")
442 }
443
444 #[must_use]
446 pub fn needs_operand(&self) -> bool {
447 self.op_index != 0
448 }
449
450 #[must_use]
452 pub fn needs_second_operand(&self) -> bool {
453 uses_number_ops(self.kind) && matches!(self.op_index, 7 | 8)
454 }
455
456 fn text_op_for_index(index: usize) -> Option<TextOp> {
457 match index {
458 1 => Some(TextOp::Contains),
459 2 => Some(TextOp::DoesNotContain),
460 3 => Some(TextOp::BeginsWith),
461 4 => Some(TextOp::EndsWith),
462 5 => Some(TextOp::Is),
463 6 => Some(TextOp::IsNot),
464 7 => Some(TextOp::Matches),
465 _ => None,
466 }
467 }
468
469 fn number_op_for_index(index: usize) -> Option<NumberOp> {
470 match index {
471 1 => Some(NumberOp::Eq),
472 2 => Some(NumberOp::Ne),
473 3 => Some(NumberOp::Gt),
474 4 => Some(NumberOp::Ge),
475 5 => Some(NumberOp::Lt),
476 6 => Some(NumberOp::Le),
477 7 => Some(NumberOp::Between),
478 8 => Some(NumberOp::NotBetween),
479 _ => None,
480 }
481 }
482
483 fn active_input_mut(&mut self) -> &mut TextInput {
484 match self.focus {
485 FilterInput::Search => &mut self.search,
486 FilterInput::OperandA => &mut self.operand_a,
487 FilterInput::OperandB => &mut self.operand_b,
488 }
489 }
490
491 #[must_use]
495 pub fn visible_indices(&self) -> Vec<usize> {
496 let needle = self.search.value.to_lowercase();
497 self.distinct
498 .iter()
499 .enumerate()
500 .filter(|(_, row)| needle.is_empty() || row.label.to_lowercase().contains(&needle))
501 .map(|(i, _)| i)
502 .collect()
503 }
504
505 #[must_use]
509 pub fn all_checked(&self) -> bool {
510 !self.distinct.is_empty() && self.distinct.iter().all(|r| r.checked)
511 }
512
513 fn to_filter(&self) -> ColumnFilter {
516 let predicate = self.build_predicate();
517 let all_checked = self.distinct.iter().all(|r| r.checked);
518 let values = if all_checked {
519 None
520 } else {
521 Some(
522 self.distinct
523 .iter()
524 .filter(|r| r.checked)
525 .map(|r| r.label.clone())
526 .collect(),
527 )
528 };
529 ColumnFilter { predicate, values }
530 }
531
532 fn build_predicate(&self) -> FilterPredicate {
533 if self.op_index == 0 {
534 return FilterPredicate::None;
535 }
536 if uses_number_ops(self.kind) {
537 let Some(op) = Self::number_op_for_index(self.op_index) else {
538 return FilterPredicate::None;
539 };
540 let Some(a) = self.parse_number_operand(&self.operand_a.value) else {
541 return FilterPredicate::None;
542 };
543 let b = if self.needs_second_operand() {
544 self.parse_number_operand(&self.operand_b.value)
545 .unwrap_or(a)
546 } else {
547 a
548 };
549 FilterPredicate::Number { op, a, b }
550 } else {
551 let Some(op) = Self::text_op_for_index(self.op_index) else {
552 return FilterPredicate::None;
553 };
554 FilterPredicate::Text {
555 op,
556 operand: self.operand_a.value.clone(),
557 }
558 }
559 }
560
561 fn parse_number_operand(&self, s: &str) -> Option<f64> {
562 let t = s.trim();
563 if t.is_empty() {
564 return None;
565 }
566 if self.kind == ColumnKind::Date {
567 return parse_ymd_to_unix(t).map(|v| v as f64);
568 }
569 t.replace(',', "").parse::<f64>().ok()
571 }
572}
573
574fn byte_index_for_char(input: &str, char_idx: usize) -> usize {
575 input
576 .char_indices()
577 .nth(char_idx)
578 .map_or(input.len(), |(idx, _)| idx)
579}
580
581fn seed_operator(kind: ColumnKind, predicate: &FilterPredicate) -> (usize, String, String) {
584 match predicate {
585 FilterPredicate::None => (0, String::new(), String::new()),
586 FilterPredicate::Text { op, operand } => {
587 (text_op_index(*op), operand.clone(), String::new())
588 }
589 FilterPredicate::Number { op, a, b } => {
590 let b_str = if matches!(op, NumberOp::Between | NumberOp::NotBetween) {
591 fmt_number_operand(kind, *b)
592 } else {
593 String::new()
594 };
595 (number_op_index(*op), fmt_number_operand(kind, *a), b_str)
596 }
597 }
598}
599
600fn text_op_index(op: TextOp) -> usize {
601 match op {
602 TextOp::Contains => 1,
603 TextOp::DoesNotContain => 2,
604 TextOp::BeginsWith => 3,
605 TextOp::EndsWith => 4,
606 TextOp::Is => 5,
607 TextOp::IsNot => 6,
608 TextOp::Matches => 7,
609 }
610}
611
612fn number_op_index(op: NumberOp) -> usize {
613 match op {
614 NumberOp::Eq => 1,
615 NumberOp::Ne => 2,
616 NumberOp::Gt => 3,
617 NumberOp::Ge => 4,
618 NumberOp::Lt => 5,
619 NumberOp::Le => 6,
620 NumberOp::Between => 7,
621 NumberOp::NotBetween => 8,
622 }
623}
624
625fn fmt_number_operand(kind: ColumnKind, v: f64) -> String {
626 if kind == ColumnKind::Date {
627 let secs = v as i64;
628 let fmt = crate::config::DateFormat {
629 format: "%Y-%m-%d".into(),
630 ..Default::default()
631 };
632 crate::format::format_date_at(secs, secs, &fmt)
633 } else {
634 v.to_string()
636 }
637}
638
639impl GridState {
640 #[must_use]
641 pub fn new(data: GridData, config: GridConfig, focus_handle: FocusHandle) -> Self {
642 let resolved_formats = config.resolve_all(&data.columns);
643 let col_count = data.columns.len();
644 let display_indices = Arc::new((0..data.rows.len()).collect::<Vec<_>>());
645 let data_rows = Arc::new(data.rows.clone());
646 let column_meta: Arc<[ColumnContext]> = data
647 .columns
648 .iter()
649 .enumerate()
650 .map(|(index, col)| ColumnContext {
651 index,
652 name: col.name.clone(),
653 kind: col.kind,
654 })
655 .collect();
656 Self {
657 data,
658 config,
659 window: None,
660 resolved_formats,
661 data_rows,
662 display_indices,
663 selection: Selection::None,
664 range_anchor: None,
665 range_active: None,
666 sort: None,
667 filters: vec![ColumnFilter::default(); col_count],
668 scroll_handle: ScrollHandle::new(),
669 focus_handle,
670 bounds: Bounds::default(),
671 row_height: 24.0,
672 header_height: 32.0,
673 row_header_width: 50.0,
674 font_size: 14.0,
675 char_width: 7.6,
676 theme: GridTheme::default(),
677 is_dragging: false,
678 drag_start: None,
679 drag_start_hit: None,
680 scroll_at_click: None,
681 last_mouse_pos: None,
682 status_bar_height: 24.0,
683 debug_bar_enabled: false,
684 click_pos: None,
685 click_hit: None,
686 hover_hit: None,
687 resizing_col: None,
688 resize_start_x: 0.0,
689 resize_start_width: 0.0,
690 context_menu: None,
691 filter_panel: None,
692 pending_action: None,
693 pending_custom_context_menu_action: None,
694 context_menu_provider: None,
695 scrollbar_drag: None,
696 scrollbar_drag_start_offset: 0.0,
697 scrollbar_drag_start_pos: 0.0,
698 window_viewport: Size::default(),
699 edge_scroll_active: false,
700 column_meta,
701 self_weak: None,
702 busy: None,
703 }
704 }
705
706 pub fn set_config(&mut self, config: GridConfig) {
707 self.config = config;
708 self.rebuild_resolved_formats();
709 self.recompute();
710 }
711
712 pub fn append_rows(&mut self, rows: Vec<Vec<CellValue>>) -> Result<(), GridDataError> {
733 let expected = self.data.columns.len();
734 let base = self.data.rows.len();
735 for (offset, row) in rows.iter().enumerate() {
736 if row.len() != expected {
737 return Err(GridDataError::RaggedRow {
738 row_index: base + offset,
739 expected,
740 actual: row.len(),
741 });
742 }
743 }
744 if rows.is_empty() {
745 return Ok(());
746 }
747 Arc::make_mut(&mut self.data_rows).extend(rows.iter().cloned());
748 self.data.rows.extend(rows);
749 if self.sort.is_some() || self.filters.iter().any(ColumnFilter::is_active) {
750 self.recompute();
751 } else {
752 Arc::make_mut(&mut self.display_indices).extend(base..self.data.rows.len());
753 }
754 if let Some(window) = &mut self.window {
755 window.total_rows += self.data.rows.len() - base;
756 }
757 Ok(())
758 }
759
760 #[must_use]
764 pub fn display_row_count(&self) -> usize {
765 self.window
766 .map(|w| w.total_rows)
767 .unwrap_or(self.display_indices.len())
768 }
769
770 #[must_use]
775 pub fn resident_row_for_display(&self, display_row: usize) -> Option<usize> {
776 match self.window {
777 Some(w) => display_row
778 .checked_sub(w.offset)
779 .filter(|r| *r < self.data.rows.len() && display_row < w.total_rows),
780 None => self.display_indices.get(display_row).copied(),
781 }
782 }
783
784 pub fn set_row_window(
796 &mut self,
797 total_rows: usize,
798 offset: usize,
799 rows: Vec<Vec<CellValue>>,
800 ) -> Result<(), GridDataError> {
801 let expected = self.data.columns.len();
802 for (i, row) in rows.iter().enumerate() {
803 if row.len() != expected {
804 return Err(GridDataError::RaggedRow {
805 row_index: offset + i,
806 expected,
807 actual: row.len(),
808 });
809 }
810 }
811 self.sort = None;
812 for filter in &mut self.filters {
813 *filter = ColumnFilter::default();
814 }
815 self.data_rows = Arc::new(rows.clone());
816 self.display_indices = Arc::new((0..rows.len()).collect());
817 self.data.rows = rows;
818 self.window = Some(RowWindow { total_rows, offset });
819 Ok(())
820 }
821
822 #[must_use]
828 pub fn visible_row_range(&self) -> (usize, usize) {
829 let total = self.display_row_count();
830 let sy: f32 = self.scroll_handle.offset().y.into();
831 let vh: f32 = self.bounds.size.height.into();
832 let visible_h = vh - self.header_height;
833 if visible_h <= 0.0 || self.row_height <= 0.0 {
834 return (0, 0);
835 }
836 let first = ((sy / self.row_height) as usize).min(total);
837 let last = (first + (visible_h / self.row_height) as usize + 1).min(total);
838 (first, last)
839 }
840
841 pub fn set_debug_bar_enabled(&mut self, enabled: bool) {
845 self.debug_bar_enabled = enabled;
846 }
847
848 #[must_use]
851 pub fn is_busy(&self) -> bool {
852 self.busy.is_some()
853 }
854
855 #[must_use]
857 pub fn busy(&self) -> Option<&BusyState> {
858 self.busy.as_ref()
859 }
860
861 pub fn set_busy(&mut self, label: impl Into<String>) {
866 self.busy = Some(BusyState {
867 label: label.into(),
868 progress: None,
869 });
870 }
871
872 pub fn set_busy_progress(&mut self, progress: f32) {
875 if let Some(b) = self.busy.as_mut() {
876 b.progress = Some(progress.clamp(0.0, 1.0));
877 }
878 }
879
880 pub fn clear_busy(&mut self) {
882 self.busy = None;
883 }
884
885 pub fn spawn_background<R, W, D>(
902 &mut self,
903 cx: &mut App,
904 label: impl Into<String>,
905 work: W,
906 on_done: D,
907 ) where
908 R: Send + 'static,
909 W: FnOnce() -> R + Send + 'static,
910 D: FnOnce(R, &mut GridState, &mut App) + 'static,
911 {
912 let Some(weak) = self.self_weak.clone() else {
913 let result = work();
915 on_done(result, self, cx);
916 return;
917 };
918
919 self.busy = Some(BusyState {
920 label: label.into(),
921 progress: None,
922 });
923
924 let background = cx.background_executor().clone();
925 cx.spawn(async move |cx| {
926 let _ = cx.update(|app| {
928 let _ = weak.update(app, |_s, c| c.notify());
929 });
930 let result = background.spawn(async move { work() }).await;
931 let _ = cx.update(|app| {
932 let _ = weak.update(app, |s, c| {
933 s.busy = None;
934 on_done(result, s, c);
935 c.notify();
936 });
937 });
938 })
939 .detach();
940 }
941
942 fn rebuild_resolved_formats(&mut self) {
943 self.resolved_formats = self.config.resolve_all(&self.data.columns);
944 }
945
946 pub fn recompute(&mut self) {
947 if self.window.is_some() {
950 self.display_indices = Arc::new((0..self.data.rows.len()).collect());
951 return;
952 }
953 let mut indices: Vec<usize> = (0..self.data.rows.len())
954 .filter(|&row_idx| {
955 self.data.columns.iter().enumerate().all(|(col_idx, _col)| {
956 let filter = &self.filters[col_idx];
957 if !filter.is_active() {
958 return true;
959 }
960 let cell = &self.data.rows[row_idx][col_idx];
961 cell_passes_filter(cell, &self.resolved_formats[col_idx], filter)
962 })
963 })
964 .collect();
965
966 if let Some((sort_col, direction)) = self.sort {
967 indices.sort_by(|&a, &b| {
968 let cell_a = &self.data.rows[a][sort_col];
969 let cell_b = &self.data.rows[b][sort_col];
970 let ord = compare_cells(cell_a, cell_b);
971 match direction {
972 SortDirection::Ascending => ord,
973 SortDirection::Descending => ord.reverse(),
974 }
975 });
976 }
977 self.display_indices = Arc::new(indices);
978 }
979
980 fn content_size(&self) -> (f32, f32) {
981 let cw: f32 = self.data.columns.iter().map(|c| c.width).sum();
982 let ch = self.display_row_count() as f32 * self.row_height;
983 (cw, ch)
984 }
985
986 pub(crate) fn max_scroll(&self) -> (f32, f32) {
987 let (cw, ch) = self.content_size();
988 let (rw, rh) = self.scrollbar_reserved();
989 let vw: f32 = self.bounds.size.width.into();
990 let vh: f32 = self.bounds.size.height.into();
991 let vw = vw - self.row_header_width - rw;
992 let vh = vh - self.header_height - rh;
993 ((cw - vw).max(0.0), (ch - vh).max(0.0))
994 }
995
996 fn scrollbar_reserved(&self) -> (f32, f32) {
997 let (cw, ch) = self.content_size();
998 let vw: f32 = self.bounds.size.width.into();
999 let vh: f32 = self.bounds.size.height.into();
1000 let vw = vw - self.row_header_width;
1001 let vh = vh - self.header_height;
1002 let reserved_w = if ch > vh { SCROLLBAR_SIZE } else { 0.0 };
1003 let reserved_h = if cw > vw { SCROLLBAR_SIZE } else { 0.0 };
1004 (reserved_w, reserved_h)
1005 }
1006
1007 fn vbar_geom(&self) -> Option<(f32, f32, f32, f32, f32)> {
1008 let (_, ch) = self.content_size();
1009 let (_, rh) = self.scrollbar_reserved();
1010 let vh: f32 = self.bounds.size.height.into();
1011 let vh = vh - self.header_height - rh;
1012 if ch <= vh {
1013 return None;
1014 }
1015 let sw: f32 = self.bounds.size.width.into();
1018 let sh: f32 = self.bounds.size.height.into();
1019 let track_x = sw - SCROLLBAR_SIZE;
1020 let track_y = self.header_height;
1021 let track_h = sh - self.header_height - rh;
1022 let thumb_h = ((track_h * (vh / ch)).max(20.0)).min(track_h);
1023 Some((track_x, track_y, SCROLLBAR_SIZE, track_h, thumb_h))
1024 }
1025
1026 fn hbar_geom(&self) -> Option<(f32, f32, f32, f32, f32)> {
1027 let (cw, _) = self.content_size();
1028 let (rw, _) = self.scrollbar_reserved();
1029 let vw: f32 = self.bounds.size.width.into();
1030 let vw = vw - self.row_header_width - rw;
1031 if cw <= vw {
1032 return None;
1033 }
1034 let sw: f32 = self.bounds.size.width.into();
1037 let sh: f32 = self.bounds.size.height.into();
1038 let track_x = self.row_header_width;
1039 let track_y = sh - SCROLLBAR_SIZE;
1040 let track_w = sw - self.row_header_width - rw;
1041 let thumb_w = ((track_w * (vw / cw)).max(20.0)).min(track_w);
1042 Some((track_x, track_y, track_w, SCROLLBAR_SIZE, thumb_w))
1043 }
1044
1045 pub(crate) fn scroll_to_vbar(&mut self, mouse_y: f32) {
1046 if let Some((_, track_y, _, track_h, thumb_h)) = self.vbar_geom() {
1047 let (_, max_y) = self.max_scroll();
1048 let range = (track_h - thumb_h).max(0.0);
1049 let rel = (mouse_y - track_y - thumb_h * 0.5).clamp(0.0, range);
1050 let frac = if range > 0.0 { rel / range } else { 0.0 };
1051 let new_y = frac * max_y;
1052 let x = self.scroll_handle.offset().x;
1053 self.scroll_handle.set_offset(Point { x, y: px(new_y) });
1054 }
1055 }
1056
1057 pub(crate) fn scroll_to_hbar(&mut self, mouse_x: f32) {
1058 if let Some((track_x, _, track_w, _, thumb_w)) = self.hbar_geom() {
1059 let (max_x, _) = self.max_scroll();
1060 let range = (track_w - thumb_w).max(0.0);
1061 let rel = (mouse_x - track_x - thumb_w * 0.5).clamp(0.0, range);
1062 let frac = if range > 0.0 { rel / range } else { 0.0 };
1063 let new_x = frac * max_x;
1064 let y = self.scroll_handle.offset().y;
1065 self.scroll_handle.set_offset(Point { x: px(new_x), y });
1066 }
1067 }
1068
1069 pub(crate) fn scroll_one_edge_tick(&mut self, dx: f32, dy: f32) {
1070 let (mx, my) = self.max_scroll();
1071 let s = self.scroll_handle.offset();
1072 let new_x: f32 = (f32::from(s.x) + dx).clamp(0.0, mx);
1073 let new_y: f32 = (f32::from(s.y) + dy).clamp(0.0, my);
1074 self.scroll_handle.set_offset(Point {
1075 x: px(new_x),
1076 y: px(new_y),
1077 });
1078 }
1079
1080 pub fn toggle_sort(&mut self, col: usize) {
1081 if self.window.is_some() {
1084 return;
1085 }
1086 self.sort = match self.sort {
1087 Some((c, SortDirection::Ascending)) if c == col => {
1088 Some((col, SortDirection::Descending))
1089 }
1090 Some((c, SortDirection::Descending)) if c == col => None,
1091 _ => Some((col, SortDirection::Ascending)),
1092 };
1093 self.recompute();
1094 }
1095
1096 pub fn handle_mouse_down(&mut self, pos: Point<Pixels>, shift: bool) {
1097 self.handle_mouse_down_with_modifiers(pos, shift, false);
1098 }
1099
1100 pub fn handle_mouse_down_with_modifiers(&mut self, pos: Point<Pixels>, shift: bool, cmd: bool) {
1101 let hit = self.hit_test(pos);
1102 self.click_pos = Some(pos);
1103 self.click_hit = Some(hit);
1104 match hit {
1105 HitResult::VerticalScrollbar => {
1106 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1107 self.scroll_to_vbar(f32::from(pos.y));
1108 self.clear_drag();
1109 }
1110 HitResult::HorizontalScrollbar => {
1111 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1112 self.scroll_to_hbar(f32::from(pos.x));
1113 self.clear_drag();
1114 }
1115 HitResult::ColumnBorder(col) => {
1116 self.resizing_col = Some(col);
1117 self.resize_start_x = f32::from(pos.x);
1118 self.resize_start_width = self.data.columns[col].width;
1119 self.clear_drag();
1120 }
1121 HitResult::ColumnHeader(col) => {
1122 if cmd {
1123 let mut cols: Vec<usize> = match &self.selection {
1126 Selection::Column(c) => vec![*c],
1127 Selection::Columns(cs) => cs.clone(),
1128 _ => Vec::new(),
1129 };
1130 if let Some(idx) = cols.iter().position(|&c| c == col) {
1131 cols.remove(idx);
1132 } else {
1133 cols.push(col);
1134 cols.sort_unstable();
1135 }
1136 self.selection = match cols.len() {
1137 0 => Selection::None,
1138 1 => Selection::Column(cols[0]),
1139 _ => Selection::Columns(cols),
1140 };
1141 self.clear_drag();
1142 } else {
1143 self.selection = Selection::Column(col);
1144 self.start_drag(pos);
1146 self.drag_start_hit = Some(HitResult::ColumnHeader(col));
1147 }
1148 }
1149 HitResult::SortButton(col) => {
1150 self.toggle_sort(col);
1153 self.clear_drag();
1154 }
1155 HitResult::ContextMenuItem(_) => {}
1156 HitResult::RowHeader(row) => {
1157 if cmd {
1158 let mut rows: Vec<usize> = match &self.selection {
1161 Selection::Row(r) => vec![*r],
1162 Selection::RowRange(r1, r2) => (*r1.min(r2)..=*r1.max(r2)).collect(),
1163 Selection::Rows(rs) => rs.clone(),
1164 _ => Vec::new(),
1165 };
1166 if let Some(idx) = rows.iter().position(|&r| r == row) {
1167 rows.remove(idx);
1168 } else {
1169 rows.push(row);
1170 rows.sort_unstable();
1171 }
1172 self.selection = match rows.len() {
1173 0 => Selection::None,
1174 1 => Selection::Row(rows[0]),
1175 _ => Selection::Rows(rows),
1176 };
1177 self.clear_drag();
1178 return;
1179 }
1180 self.selection = if shift {
1181 if let Selection::Row(prev) = self.selection {
1182 let (s, e) = (prev, row);
1183 Selection::RowRange(s.min(e), s.max(e))
1184 } else {
1185 Selection::Row(row)
1186 }
1187 } else {
1188 Selection::Row(row)
1189 };
1190 self.start_drag(pos);
1191 self.drag_start_hit = Some(HitResult::RowHeader(row));
1192 }
1193 HitResult::Cell(row, col) => {
1194 if cmd {
1195 let mut cells: Vec<(usize, usize)> = match &self.selection {
1198 Selection::Cell(r, c) => vec![(*r, *c)],
1199 Selection::Cells(cs) => cs.clone(),
1200 _ => Vec::new(),
1201 };
1202 if let Some(idx) = cells.iter().position(|&rc| rc == (row, col)) {
1203 cells.remove(idx);
1204 } else {
1205 cells.push((row, col));
1206 cells.sort_unstable();
1207 }
1208 self.selection = match cells.len() {
1209 0 => Selection::None,
1210 1 => Selection::Cell(cells[0].0, cells[0].1),
1211 _ => Selection::Cells(cells),
1212 };
1213 self.range_anchor = None;
1214 self.range_active = None;
1215 self.clear_drag();
1216 return;
1217 }
1218 self.selection = if shift {
1219 let anchor = self
1221 .range_anchor
1222 .or(match self.selection {
1223 Selection::Cell(pr, pc) => Some((pr, pc)),
1224 _ => None,
1225 })
1226 .unwrap_or((row, col));
1227 self.range_anchor = Some(anchor);
1228 self.range_active = Some((row, col));
1229 Selection::CellRange(
1230 anchor.0.min(row),
1231 anchor.1.min(col),
1232 anchor.0.max(row),
1233 anchor.1.max(col),
1234 )
1235 } else {
1236 self.range_anchor = Some((row, col));
1237 self.range_active = Some((row, col));
1238 Selection::Cell(row, col)
1239 };
1240 self.start_drag(pos);
1241 self.drag_start_hit = Some(HitResult::Cell(row, col));
1242 }
1243 HitResult::Corner | HitResult::None => {
1244 self.selection = Selection::None;
1245 self.range_anchor = None;
1246 self.range_active = None;
1247 self.context_menu = None;
1248 self.filter_panel = None;
1249 self.clear_drag();
1250 }
1251 }
1252 }
1253
1254 fn start_drag(&mut self, pos: Point<Pixels>) {
1255 self.is_dragging = false;
1256 self.drag_start = Some(pos);
1257 self.scroll_at_click = Some(self.scroll_handle.offset());
1258 self.last_mouse_pos = Some(pos);
1259 }
1260
1261 pub(crate) fn open_context_menu(&mut self, col: usize, anchor: Point<Pixels>) {
1262 self.context_menu = Some(menu_mod::ContextMenu::standard(col, anchor));
1263 self.filter_panel = None;
1264 }
1265
1266 pub(crate) fn context_menu_target_from_hit(&self, hit: HitResult) -> Option<ContextMenuTarget> {
1269 match hit {
1270 HitResult::Cell(row, col) => {
1271 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1272 Some(ContextMenuTarget::Cell {
1273 display_row_index: row,
1274 source_row_index: source_row,
1275 column_index: col,
1276 })
1277 }
1278 HitResult::RowHeader(row) => {
1279 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1280 Some(ContextMenuTarget::RowHeader {
1281 display_row_index: row,
1282 source_row_index: source_row,
1283 })
1284 }
1285 HitResult::ColumnHeader(col) => {
1286 Some(ContextMenuTarget::ColumnHeader { column_index: col })
1287 }
1288 HitResult::SortButton(col) => Some(ContextMenuTarget::SortButton { column_index: col }),
1289 _ => None,
1290 }
1291 }
1292
1293 pub(crate) fn effective_selection_for_context_target(
1298 &self,
1299 target: &ContextMenuTarget,
1300 ) -> Selection {
1301 match target {
1302 ContextMenuTarget::Cell {
1303 display_row_index,
1304 column_index,
1305 ..
1306 } => {
1307 if is_cell_selected(&self.selection, *display_row_index, *column_index) {
1308 self.selection.clone()
1309 } else {
1310 Selection::Cell(*display_row_index, *column_index)
1311 }
1312 }
1313 ContextMenuTarget::RowHeader {
1314 display_row_index, ..
1315 } => {
1316 if is_row_selected(&self.selection, *display_row_index) {
1317 self.selection.clone()
1318 } else {
1319 Selection::Row(*display_row_index)
1320 }
1321 }
1322 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. } => {
1323 self.selection.clone()
1324 }
1325 }
1326 }
1327
1328 pub(crate) fn build_context_menu_request(
1340 &self,
1341 target: ContextMenuTarget,
1342 selection: &Selection,
1343 ) -> ContextMenuRequest {
1344 let mut target = target;
1351 let mut selection = selection.clone();
1352 if let Some(w) = self.window {
1353 let resident_last = self.data.rows.len().saturating_sub(1);
1354 let to_resident = |dr: usize| dr.saturating_sub(w.offset).min(resident_last);
1355 target = match target {
1356 ContextMenuTarget::Cell {
1357 display_row_index,
1358 source_row_index,
1359 column_index,
1360 } => ContextMenuTarget::Cell {
1361 display_row_index: to_resident(display_row_index),
1362 source_row_index,
1363 column_index,
1364 },
1365 ContextMenuTarget::RowHeader {
1366 display_row_index,
1367 source_row_index,
1368 } => ContextMenuTarget::RowHeader {
1369 display_row_index: to_resident(display_row_index),
1370 source_row_index,
1371 },
1372 other => other,
1373 };
1374 selection = match selection {
1375 Selection::Cell(r, c) => Selection::Cell(to_resident(r), c),
1376 Selection::Row(r) => Selection::Row(to_resident(r)),
1377 Selection::CellRange(r1, c1, r2, c2) => {
1378 Selection::CellRange(to_resident(r1), c1, to_resident(r2), c2)
1379 }
1380 other => other,
1381 };
1382 }
1383 let selection = &selection;
1384
1385 let nrows = self.display_indices.len();
1386 let ncols = self.data.columns.len();
1387
1388 let (r1, c1, r2, c2) = match selection.normalized_bounds() {
1389 Some((r1, c1, r2, c2)) => {
1390 let r1 = r1.min(nrows.saturating_sub(1));
1391 let r2 = r2.min(nrows.saturating_sub(1));
1392 let c1 = c1.min(ncols.saturating_sub(1));
1393 let c2 = c2.min(ncols.saturating_sub(1));
1394 (r1, c1, r2, c2)
1395 }
1396 None => match &target {
1397 ContextMenuTarget::Cell {
1398 display_row_index,
1399 column_index,
1400 ..
1401 } => (
1402 *display_row_index,
1403 *column_index,
1404 *display_row_index,
1405 *column_index,
1406 ),
1407 ContextMenuTarget::RowHeader {
1408 display_row_index, ..
1409 } => (
1410 *display_row_index,
1411 0,
1412 *display_row_index,
1413 ncols.saturating_sub(1),
1414 ),
1415 ContextMenuTarget::ColumnHeader { column_index }
1416 | ContextMenuTarget::SortButton { column_index } => {
1417 (0, *column_index, nrows.saturating_sub(1), *column_index)
1418 }
1419 },
1420 };
1421
1422 let menu_selection = ContextMenuSelection {
1423 row_start: r1,
1424 row_end: r2,
1425 column_start: c1,
1426 column_end: c2,
1427 };
1428
1429 let column_oriented =
1434 matches!(
1435 target,
1436 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. }
1437 ) || matches!(selection, Selection::Column(_) | Selection::Columns(_));
1438
1439 ContextMenuRequest::new(
1440 target,
1441 Some(menu_selection),
1442 Arc::clone(&self.data_rows),
1443 Arc::clone(&self.display_indices),
1444 Arc::clone(&self.column_meta),
1445 column_oriented,
1446 )
1447 }
1448
1449 pub(crate) fn execute_custom_context_menu_action(
1453 &mut self,
1454 pending: PendingCustomContextMenuAction,
1455 cx: &mut App,
1456 ) {
1457 self.context_menu = None;
1458 self.filter_panel = None;
1459
1460 let Some(provider) = self.context_menu_provider.clone() else {
1461 return;
1462 };
1463
1464 provider.on_action(&pending.id, &pending.request, self, cx);
1465 }
1466
1467 pub(crate) fn convert_context_menu_items(items: Vec<ContextMenuItem>) -> Vec<MenuItem> {
1470 items
1471 .into_iter()
1472 .map(|item| match item {
1473 ContextMenuItem::BuiltIn(action) => MenuItem::Action(action),
1474 ContextMenuItem::Action { id, label } => MenuItem::Custom { id, label },
1475 ContextMenuItem::Separator => MenuItem::Separator,
1476 })
1477 .collect()
1478 }
1479
1480 pub fn execute_action(&mut self, action: MenuAction, col: usize, cx: &mut App) {
1481 match action {
1482 MenuAction::SelectColumn => {
1483 self.selection = Selection::Column(col);
1484 }
1485 MenuAction::CopyColumn => {
1486 let text = self.column_text(col);
1487 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1488 }
1489 MenuAction::CopyColumnWithHeaders => {
1490 let mut text = String::new();
1491 text.push_str(&self.data.columns[col].name);
1492 text.push('\n');
1493 text.push_str(&self.column_text(col));
1494 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1495 }
1496 MenuAction::SortAscending => {
1497 self.sort = Some((col, SortDirection::Ascending));
1498 self.recompute();
1499 }
1500 MenuAction::SortDescending => {
1501 self.sort = Some((col, SortDirection::Descending));
1502 self.recompute();
1503 }
1504 MenuAction::ClearSort => {
1505 self.sort = None;
1506 self.recompute();
1507 }
1508 MenuAction::FilterPrompt => {
1509 let anchor = self.context_menu.as_ref().map(|m| m.anchor);
1510 self.open_filter_panel(col, anchor);
1511 }
1512 MenuAction::ClearFilter => {
1513 if col < self.filters.len() {
1514 self.filters[col] = ColumnFilter::default();
1515 self.recompute();
1516 }
1517 }
1518 }
1519 self.context_menu = None;
1520 }
1521
1522 pub fn open_filter_panel(&mut self, col: usize, _anchor: Option<Point<Pixels>>) {
1533 if col >= self.data.columns.len() {
1534 return;
1535 }
1536 let sx = f32::from(self.scroll_handle.offset().x);
1537 let col_x = self.row_header_width
1538 + self.data.columns[..col]
1539 .iter()
1540 .map(|c| c.width)
1541 .sum::<f32>()
1542 - sx;
1543 let anchor = Point {
1544 x: px(col_x + self.data.columns[col].width * 0.5),
1545 y: px(0.0),
1546 };
1547 let kind = self.data.columns[col].kind;
1548 let existing = self.filters.get(col).cloned().unwrap_or_default();
1549
1550 let distinct = {
1552 let fmt = &self.resolved_formats[col];
1553 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1554 let mut pairs: Vec<(String, &CellValue)> = Vec::new();
1555 for row in &self.data.rows {
1556 let cell = &row[col];
1557 let (label, _) = format_cell(cell, fmt);
1558 if seen.insert(label.clone()) {
1559 pairs.push((label, cell));
1560 }
1561 }
1562 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
1563 pairs
1564 .into_iter()
1565 .map(|(label, _)| {
1566 let checked = match &existing.values {
1567 None => true,
1568 Some(set) => set.contains(&label),
1569 };
1570 FilterValueRow { label, checked }
1571 })
1572 .collect()
1573 };
1574
1575 let (op_index, operand_a, operand_b) = seed_operator(kind, &existing.predicate);
1576
1577 self.context_menu = None;
1578 self.filter_panel = Some(FilterPanel {
1579 col,
1580 anchor,
1581 kind,
1582 search: TextInput::default(),
1583 op_index,
1584 op_menu_open: false,
1585 operand_a: TextInput::new(operand_a),
1586 operand_b: TextInput::new(operand_b),
1587 focus: FilterInput::Search,
1588 auto_apply: true,
1589 distinct,
1590 });
1591 }
1592
1593 pub fn apply_filter_panel(&mut self) {
1596 let Some(panel) = &self.filter_panel else {
1597 return;
1598 };
1599 let col = panel.col;
1600 let filter = panel.to_filter();
1601 if col < self.filters.len() {
1602 self.filters[col] = filter;
1603 self.recompute();
1604 }
1605 }
1606
1607 pub fn maybe_auto_apply(&mut self) {
1609 if self.filter_panel.is_some() {
1610 self.apply_filter_panel();
1611 }
1612 }
1613
1614 pub fn clear_filter_panel(&mut self) {
1617 let mut target_col = None;
1618 if let Some(panel) = &mut self.filter_panel {
1619 panel.op_index = 0;
1620 panel.op_menu_open = false;
1621 panel.operand_a = TextInput::default();
1622 panel.operand_b = TextInput::default();
1623 panel.search = TextInput::default();
1624 for row in &mut panel.distinct {
1625 row.checked = true;
1626 }
1627 target_col = Some(panel.col);
1628 }
1629 if let Some(col) = target_col {
1630 if col < self.filters.len() {
1631 self.filters[col] = ColumnFilter::default();
1632 }
1633 }
1634 self.recompute();
1635 }
1636
1637 pub fn set_panel_sort(&mut self, direction: SortDirection) {
1640 if let Some(panel) = &self.filter_panel {
1641 let col = panel.col;
1642 self.sort = match self.sort {
1643 Some((c, d)) if c == col && d == direction => None,
1644 _ => Some((col, direction)),
1645 };
1646 self.recompute();
1647 }
1648 }
1649
1650 pub fn toggle_filter_value(&mut self, index: usize) {
1653 if let Some(panel) = &mut self.filter_panel {
1654 if let Some(row) = panel.distinct.get_mut(index) {
1655 row.checked = !row.checked;
1656 }
1657 }
1658 self.maybe_auto_apply();
1659 }
1660
1661 pub fn toggle_filter_select_all(&mut self) {
1666 if let Some(panel) = &mut self.filter_panel {
1667 let target = !panel.all_checked();
1668 for row in &mut panel.distinct {
1669 row.checked = target;
1670 }
1671 }
1672 self.maybe_auto_apply();
1673 }
1674
1675 pub fn set_filter_operator(&mut self, op_index: usize) {
1678 if let Some(panel) = &mut self.filter_panel {
1679 panel.op_index = op_index;
1680 panel.op_menu_open = false;
1681 if op_index != 0 {
1682 panel.focus = FilterInput::OperandA;
1683 }
1684 }
1685 self.maybe_auto_apply();
1686 }
1687
1688 pub fn toggle_filter_op_menu(&mut self) {
1690 if let Some(panel) = &mut self.filter_panel {
1691 panel.op_menu_open = !panel.op_menu_open;
1692 }
1693 }
1694
1695 pub fn set_filter_focus(&mut self, focus: FilterInput) {
1697 if let Some(panel) = &mut self.filter_panel {
1698 panel.focus = focus;
1699 }
1700 }
1701
1702 pub fn toggle_filter_auto_apply(&mut self) {
1704 if let Some(panel) = &mut self.filter_panel {
1705 panel.auto_apply = !panel.auto_apply;
1706 }
1707 self.maybe_auto_apply();
1708 }
1709
1710 fn column_text(&self, col: usize) -> String {
1711 let mut text = String::new();
1712 let fmt = &self.resolved_formats[col];
1713 for &row_idx in self.display_indices.iter() {
1714 let cell = &self.data.rows[row_idx][col];
1715 let (s, _) = format_cell(cell, fmt);
1716 text.push_str(&s);
1717 text.push('\n');
1718 }
1719 text
1720 }
1721
1722 fn clear_drag(&mut self) {
1723 self.is_dragging = false;
1724 self.drag_start = None;
1725 self.drag_start_hit = None;
1726 self.scroll_at_click = None;
1727 }
1728
1729 fn drag_world_corners(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
1730 let start = self.drag_start?;
1731 let mouse = self.last_mouse_pos?;
1732 let click_scroll = self
1733 .scroll_at_click
1734 .unwrap_or_else(|| self.scroll_handle.offset());
1735 let scroll = self.scroll_handle.offset();
1736 let sx_click: f32 = click_scroll.x.into();
1737 let sy_click: f32 = click_scroll.y.into();
1738 let sx: f32 = scroll.x.into();
1739 let sy: f32 = scroll.y.into();
1740 let sx0: f32 = start.x.into();
1741 let sy0: f32 = start.y.into();
1742 let mx: f32 = mouse.x.into();
1743 let my: f32 = mouse.y.into();
1744 let start_world = Point {
1745 x: px(sx0 + sx_click),
1746 y: px(sy0 + sy_click),
1747 };
1748 let end_world = Point {
1749 x: px(mx + sx),
1750 y: px(my + sy),
1751 };
1752 Some((start_world, end_world))
1753 }
1754
1755 pub fn drag_screen_rect(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
1756 if !self.is_dragging {
1757 return None;
1758 }
1759 let (start_world, end_world) = self.drag_world_corners()?;
1760 let scroll = self.scroll_handle.offset();
1761 let sx: f32 = scroll.x.into();
1762 let sy: f32 = scroll.y.into();
1763 let start_screen = Point {
1764 x: px(f32::from(start_world.x) - sx),
1765 y: px(f32::from(start_world.y) - sy),
1766 };
1767 let end_screen = Point {
1768 x: px(f32::from(end_world.x) - sx),
1769 y: px(f32::from(end_world.y) - sy),
1770 };
1771 Some((start_screen, end_screen))
1772 }
1773
1774 fn update_drag(&mut self) {
1775 let (start_world, end_world) = match self.drag_world_corners() {
1776 Some(c) => c,
1777 None => return,
1778 };
1779 if !self.is_dragging {
1780 let dx = f32::from(end_world.x) - f32::from(start_world.x);
1781 let dy = f32::from(end_world.y) - f32::from(start_world.y);
1782 if dx * dx + dy * dy <= 400.0 {
1783 return;
1784 }
1785 self.is_dragging = true;
1786 }
1787 let r1 = match self.drag_start_hit {
1788 Some(h) => h,
1789 None => return,
1790 };
1791 let r2 = self.hit_test_content(f32::from(end_world.x), f32::from(end_world.y), 0.0, 0.0);
1795 match (r1, r2) {
1796 (HitResult::Cell(r1c, c1), HitResult::Cell(r2c, c2)) => {
1797 self.selection =
1798 Selection::CellRange(r1c.min(r2c), c1.min(c2), r1c.max(r2c), c1.max(c2));
1799 }
1800 (HitResult::RowHeader(r1r), HitResult::RowHeader(r2r)) => {
1801 self.selection = Selection::RowRange(r1r.min(r2r), r1r.max(r2r));
1802 }
1803 (
1804 HitResult::ColumnHeader(c1),
1805 HitResult::ColumnHeader(c2)
1806 | HitResult::SortButton(c2)
1807 | HitResult::ColumnBorder(c2),
1808 ) => {
1809 self.selection = if c1 == c2 {
1810 Selection::Column(c1)
1811 } else {
1812 Selection::Columns((c1.min(c2)..=c1.max(c2)).collect())
1813 };
1814 }
1815 _ => {}
1816 }
1817 }
1818
1819 fn update_drag_from_last(&mut self) {
1820 self.update_drag();
1821 }
1822
1823 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, pressed_button: Option<MouseButton>) {
1824 if self.is_dragging && pressed_button != Some(MouseButton::Left) {
1825 self.handle_mouse_up();
1826 return;
1827 }
1828 if let Some(col) = self.resizing_col {
1829 if pressed_button != Some(MouseButton::Left) {
1830 self.resizing_col = None;
1831 return;
1832 }
1833 let new_w =
1834 (self.resize_start_width + (f32::from(pos.x) - self.resize_start_x)).max(40.0);
1835 self.data.columns[col].width = new_w;
1836 return;
1837 }
1838 if let Some(axis) = self.scrollbar_drag {
1839 if pressed_button != Some(MouseButton::Left) {
1840 self.scrollbar_drag = None;
1841 return;
1842 }
1843 match axis {
1844 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
1845 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
1846 }
1847 self.last_mouse_pos = Some(pos);
1848 return;
1849 }
1850 self.last_mouse_pos = Some(pos);
1851 if self.context_menu.is_some() {
1852 return;
1857 }
1858 self.hover_hit = Some(self.hit_test(pos));
1859 if self.drag_start.is_none() {
1860 return;
1861 }
1862 self.update_drag();
1863 }
1864
1865 pub fn handle_scroll_drag(&mut self) {
1866 if self.drag_start.is_some() && self.last_mouse_pos.is_some() {
1867 self.update_drag();
1868 }
1869 }
1870
1871 pub fn handle_mouse_up(&mut self) {
1872 self.resizing_col = None;
1873 self.scrollbar_drag = None;
1874 self.clear_drag();
1875 }
1876
1877 pub fn apply_edge_scroll(&mut self) -> bool {
1878 apply_edge_scroll(self)
1879 }
1880
1881 pub fn select_all(&mut self) {
1882 let nrows = self.display_row_count();
1883 let ncols = self.data.columns.len();
1884 if nrows > 0 && ncols > 0 {
1885 self.selection = Selection::CellRange(0, 0, nrows - 1, ncols - 1);
1886 }
1887 }
1888
1889 #[must_use]
1893 pub fn selected_rows(&self) -> Vec<usize> {
1894 let nrows = self.display_row_count();
1895 match &self.selection {
1896 Selection::Row(r) if *r < nrows => vec![*r],
1897 Selection::RowRange(r1, r2) => {
1898 (*r1.min(r2)..=*r1.max(r2)).filter(|&r| r < nrows).collect()
1899 }
1900 Selection::Rows(rows) => rows.iter().copied().filter(|&r| r < nrows).collect(),
1901 _ => Vec::new(),
1902 }
1903 }
1904
1905 #[must_use]
1909 pub fn selected_columns(&self) -> Vec<usize> {
1910 let ncols = self.data.columns.len();
1911 match &self.selection {
1912 Selection::Column(c) if *c < ncols => vec![*c],
1913 Selection::Columns(cols) => cols.iter().copied().filter(|&c| c < ncols).collect(),
1914 _ => Vec::new(),
1915 }
1916 }
1917
1918 #[must_use]
1923 pub fn selected_cells(&self) -> Vec<(usize, usize)> {
1924 let nrows = self.display_row_count();
1925 let ncols = self.data.columns.len();
1926 if nrows == 0 || ncols == 0 {
1927 return Vec::new();
1928 }
1929 match &self.selection {
1930 Selection::None => Vec::new(),
1931 Selection::Cell(r, c) if *r < nrows && *c < ncols => vec![(*r, *c)],
1932 Selection::Cell(..) => Vec::new(),
1933 Selection::Cells(cells) => cells
1934 .iter()
1935 .copied()
1936 .filter(|&(r, c)| r < nrows && c < ncols)
1937 .collect(),
1938 Selection::Column(_) | Selection::Columns(_) => {
1939 let cols = self.selected_columns();
1940 (0..nrows)
1941 .flat_map(|r| cols.iter().map(move |&c| (r, c)))
1942 .collect()
1943 }
1944 Selection::Row(_) | Selection::RowRange(..) | Selection::Rows(_) => self
1945 .selected_rows()
1946 .into_iter()
1947 .flat_map(|r| (0..ncols).map(move |c| (r, c)))
1948 .collect(),
1949 Selection::CellRange(r1, c1, r2, c2) => {
1950 let (rmin, rmax) = (*r1.min(r2), *r1.max(r2));
1951 let (cmin, cmax) = (*c1.min(c2), *c1.max(c2));
1952 (rmin..=rmax.min(nrows.saturating_sub(1)))
1953 .flat_map(|r| (cmin..=cmax.min(ncols.saturating_sub(1))).map(move |c| (r, c)))
1954 .collect()
1955 }
1956 }
1957 }
1958
1959 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
1960 let Some((raw_r1, raw_c1, raw_r2, raw_c2)) = self.selection.normalized_bounds() else {
1961 return;
1962 };
1963 if self.display_row_count() == 0 || self.data.columns.is_empty() {
1964 return;
1965 }
1966 let last_row = self.display_row_count() - 1;
1967 let last_col = self.data.columns.len() - 1;
1968 let r1 = raw_r1.min(last_row);
1969 let r2 = raw_r2.min(last_row);
1970 let c1 = raw_c1.min(last_col);
1971 let c2 = raw_c2.min(last_col);
1972 let mut text = String::new();
1973 if with_headers {
1974 for c in c1..=c2 {
1975 if c > c1 {
1976 text.push('\t');
1977 }
1978 text.push_str(&self.data.columns[c].name);
1979 }
1980 text.push('\n');
1981 }
1982 for dr in r1..=r2 {
1983 let Some(row_idx) = self.resident_row_for_display(dr) else {
1984 continue;
1985 };
1986 for c in c1..=c2 {
1987 if c > c1 {
1988 text.push('\t');
1989 }
1990 let cell = &self.data.rows[row_idx][c];
1991 let (s, _) = format_cell(cell, &self.resolved_formats[c]);
1992 text.push_str(&s);
1993 }
1994 text.push('\n');
1995 }
1996 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1997 }
1998
1999 pub fn page_up(&mut self) {
2000 let vh: f32 = self.bounds.size.height.into();
2001 let rows = ((vh - self.header_height) / self.row_height) as i32;
2002 self.move_selection(0, -rows);
2003 }
2004
2005 pub fn page_down(&mut self) {
2006 let vh: f32 = self.bounds.size.height.into();
2007 let rows = ((vh - self.header_height) / self.row_height) as i32;
2008 self.move_selection(0, rows);
2009 }
2010
2011 pub fn handle_key(&mut self, keystroke: &Keystroke) {
2012 if self.filter_panel.is_some() {
2013 match keystroke.key.as_str() {
2014 "escape" => {
2015 self.filter_panel = None;
2016 return;
2017 }
2018 "enter" => {
2019 self.apply_filter_panel();
2020 return;
2021 }
2022 _ => {}
2023 }
2024 let mut edited = false;
2025 if let Some(panel) = &mut self.filter_panel {
2026 let input = panel.active_input_mut();
2027 match keystroke.key.as_str() {
2028 "backspace" => {
2029 input.backspace();
2030 edited = true;
2031 }
2032 "left" => input.move_left(),
2033 "right" => input.move_right(),
2034 _ => {
2035 if let Some(ch) = keystroke_to_char(keystroke) {
2036 input.insert_char(ch);
2037 edited = true;
2038 }
2039 }
2040 }
2041 }
2042 if edited {
2045 self.maybe_auto_apply();
2046 }
2047 return;
2048 }
2049 if self.context_menu.is_some() {
2050 if keystroke.key.as_str() == "escape" {
2051 self.context_menu = None;
2052 }
2053 return;
2054 }
2055 let shift = keystroke.modifiers.shift;
2056 match keystroke.key.as_str() {
2057 "up" if shift => self.extend_selection(0, -1),
2058 "down" if shift => self.extend_selection(0, 1),
2059 "left" if shift => self.extend_selection(-1, 0),
2060 "right" if shift => self.extend_selection(1, 0),
2061 "up" => self.move_selection(0, -1),
2062 "down" => self.move_selection(0, 1),
2063 "left" => self.move_selection(-1, 0),
2064 "right" => self.move_selection(1, 0),
2065 "escape" => {
2066 self.selection = Selection::None;
2067 self.range_anchor = None;
2068 self.range_active = None;
2069 }
2070 _ => {}
2071 }
2072 }
2073
2074 fn move_selection(&mut self, dx: i32, dy: i32) {
2075 let nrows = self.display_row_count() as i32;
2076 let ncols = self.data.columns.len() as i32;
2077 if nrows == 0 || ncols == 0 {
2078 return;
2079 }
2080 let last_row = nrows - 1;
2081 let last_col = ncols - 1;
2082 match self.selection {
2083 Selection::Cell(row, col) => {
2084 let nr = (row as i32 + dy).clamp(0, last_row) as usize;
2085 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
2086 self.selection = Selection::Cell(nr, nc);
2087 self.range_anchor = Some((nr, nc));
2088 self.range_active = Some((nr, nc));
2089 }
2090 Selection::Row(row) if dy != 0 => {
2091 let nr = (row as i32 + dy).clamp(0, last_row) as usize;
2092 self.selection = Selection::Row(nr);
2093 }
2094 Selection::Column(col) if dx != 0 => {
2095 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
2096 self.selection = Selection::Column(nc);
2097 }
2098 _ => {
2099 self.selection = Selection::Cell(0, 0);
2100 self.range_anchor = Some((0, 0));
2101 self.range_active = Some((0, 0));
2102 }
2103 }
2104 }
2105
2106 fn extend_selection(&mut self, dx: i32, dy: i32) {
2110 let nrows = self.display_row_count() as i32;
2111 let ncols = self.data.columns.len() as i32;
2112 if nrows == 0 || ncols == 0 {
2113 return;
2114 }
2115 let last_row = nrows - 1;
2116 let last_col = ncols - 1;
2117
2118 if self.range_anchor.is_none() || self.range_active.is_none() {
2120 match self.selection {
2121 Selection::Cell(r, c) => {
2122 self.range_anchor = Some((r, c));
2123 self.range_active = Some((r, c));
2124 }
2125 Selection::CellRange(r1, c1, r2, c2) => {
2126 self.range_anchor = Some((r1, c1));
2127 self.range_active = Some((r2, c2));
2128 }
2129 _ => {
2130 self.range_anchor = Some((0, 0));
2131 self.range_active = Some((0, 0));
2132 self.selection = Selection::Cell(0, 0);
2133 }
2134 }
2135 }
2136
2137 let anchor = self.range_anchor.unwrap_or((0, 0));
2138 let active = self.range_active.unwrap_or(anchor);
2139 let nr = (active.0 as i32 + dy).clamp(0, last_row) as usize;
2140 let nc = (active.1 as i32 + dx).clamp(0, last_col) as usize;
2141 self.range_active = Some((nr, nc));
2142
2143 self.selection = if (nr, nc) == anchor {
2144 Selection::Cell(nr, nc)
2145 } else {
2146 Selection::CellRange(
2147 anchor.0.min(nr),
2148 anchor.1.min(nc),
2149 anchor.0.max(nr),
2150 anchor.1.max(nc),
2151 )
2152 };
2153 }
2154
2155 pub(crate) fn hit_test(&self, pos: Point<Pixels>) -> HitResult {
2156 let bounds = self.bounds;
2157 let (sx, sy) = (
2158 f32::from(self.scroll_handle.offset().x),
2159 f32::from(self.scroll_handle.offset().y),
2160 );
2161 let bw: f32 = bounds.size.width.into();
2162 let bh: f32 = bounds.size.height.into();
2163 let (mx, my) = self.max_scroll();
2164 if let Some(menu) = &self.context_menu {
2165 let cw = self.char_width;
2166 let x_rel = f32::from(pos.x);
2169 let y_rel = f32::from(pos.y);
2170 if let Some(idx) = menu_mod::hover_at(menu, x_rel, y_rel, cw) {
2171 return HitResult::ContextMenuItem(idx);
2172 }
2173 }
2174 if my > 0.0
2175 && f32::from(pos.x) >= bw - SCROLLBAR_SIZE
2176 && f32::from(pos.y) >= self.header_height
2177 {
2178 return HitResult::VerticalScrollbar;
2179 }
2180 if mx > 0.0
2181 && f32::from(pos.y) >= bh - SCROLLBAR_SIZE
2182 && f32::from(pos.x) >= self.row_header_width
2183 {
2184 return HitResult::HorizontalScrollbar;
2185 }
2186 let px = f32::from(pos.x);
2192 let py = f32::from(pos.y);
2193 if px < 0.0 || py < 0.0 || px > bw || py > bh {
2194 return HitResult::None;
2195 }
2196 self.hit_test_content(px, py, sx, sy)
2197 }
2198
2199 fn hit_test_content(&self, x: f32, y: f32, sx: f32, sy: f32) -> HitResult {
2200 if y < self.header_height {
2201 if x < self.row_header_width {
2202 return HitResult::Corner;
2203 }
2204 let col_x = x - self.row_header_width + sx;
2205 let mut acc = 0.0;
2206 for (i, col) in self.data.columns.iter().enumerate() {
2207 let right = acc + col.width;
2208 if i + 1 < self.data.columns.len() && col_x >= right - 5.0 && col_x <= right + 5.0 {
2209 return HitResult::ColumnBorder(i);
2210 }
2211 if col_x >= acc && col_x < right {
2212 if col_x >= right - 20.0 {
2213 return HitResult::SortButton(i);
2214 }
2215 return HitResult::ColumnHeader(i);
2216 }
2217 acc = right;
2218 }
2219 return HitResult::None;
2220 }
2221 if x < self.row_header_width {
2222 let row_y = y - self.header_height + sy;
2223 if row_y < 0.0 {
2224 return HitResult::None;
2225 }
2226 let row_idx = (row_y / self.row_height) as usize;
2227 if row_idx < self.display_row_count() {
2228 return HitResult::RowHeader(row_idx);
2229 }
2230 return HitResult::None;
2231 }
2232 let col_x = x - self.row_header_width + sx;
2233 let row_y = y - self.header_height + sy;
2234 if row_y < 0.0 {
2235 return HitResult::None;
2236 }
2237 let row_idx = (row_y / self.row_height) as usize;
2238 if row_idx >= self.display_row_count() {
2239 return HitResult::None;
2240 }
2241 let mut acc = 0.0;
2242 for (i, col) in self.data.columns.iter().enumerate() {
2243 if col_x >= acc && col_x < acc + col.width {
2244 return HitResult::Cell(row_idx, i);
2245 }
2246 acc += col.width;
2247 }
2248 HitResult::None
2249 }
2250
2251 #[must_use]
2252 pub fn wants_edge_scroll_tick(&self) -> bool {
2253 self.is_dragging
2254 }
2255}
2256
2257fn keystroke_to_char(k: &Keystroke) -> Option<char> {
2258 if k.modifiers.control || k.modifiers.platform || k.modifiers.alt {
2259 return None;
2260 }
2261 if let Some(key_char) = k.key_char.as_ref() {
2262 return key_char.chars().next();
2263 }
2264 if k.key.chars().count() == 1 {
2265 let c = k.key.chars().next()?;
2266 if k.modifiers.shift {
2267 Some(c.to_ascii_uppercase())
2268 } else {
2269 Some(c)
2270 }
2271 } else {
2272 None
2273 }
2274}
2275
2276#[cfg(test)]
2277#[allow(
2278 clippy::unwrap_used,
2279 clippy::expect_used,
2280 clippy::field_reassign_with_default
2281)]
2282mod tests {
2283 use super::*;
2284 use crate::data::{CellValue, Column, ColumnKind};
2285 use crate::grid::state::state_inner::{edge_scroll_speed, format_current_status};
2286
2287 fn input_with(text: &str, cursor: usize) -> TextInput {
2288 let mut p = TextInput::new(text.to_owned());
2289 p.cursor_chars = cursor;
2290 p
2291 }
2292
2293 #[test]
2294 fn text_input_new_cursors_at_char_count_not_bytes() {
2295 let p = TextInput::new("hé🙂".into());
2297 assert_eq!(p.cursor_chars, 3);
2298 assert_eq!(p.value.len(), 7);
2299 }
2300
2301 #[test]
2302 fn text_input_insert_emoji_at_start_does_not_panic() {
2303 let mut p = input_with("ab", 0);
2304 p.insert_char('\u{1F600}');
2305 assert_eq!(p.value, "\u{1F600}ab");
2306 assert_eq!(p.cursor_chars, 1);
2307 }
2308
2309 #[test]
2310 fn text_input_insert_in_middle_keeps_cursor_at_char_position() {
2311 let mut p = input_with("helloworld", 5);
2312 p.insert_char(' ');
2313 assert_eq!(p.value, "hello world");
2314 assert_eq!(p.cursor_chars, 6);
2315 }
2316
2317 #[test]
2318 fn text_input_backspace_at_zero_is_noop() {
2319 let mut p = input_with("abc", 0);
2320 p.backspace();
2321 assert_eq!(p.value, "abc");
2322 assert_eq!(p.cursor_chars, 0);
2323 }
2324
2325 #[test]
2326 fn text_input_backspace_removes_one_char_value() {
2327 let mut p = input_with("héx", 2);
2329 p.backspace();
2330 assert_eq!(p.value, "hx");
2331 assert_eq!(p.cursor_chars, 1);
2332 }
2333
2334 #[test]
2335 fn text_input_clamp_cursor_pulls_back_past_end() {
2336 let mut p = input_with("abc", 99);
2337 p.clamp_cursor();
2338 assert_eq!(p.cursor_chars, 3);
2339 }
2340
2341 #[test]
2342 fn text_input_move_left_and_right_respect_bounds() {
2343 let mut p = input_with("ab", 2);
2344 p.move_right();
2345 assert_eq!(p.cursor_chars, 2);
2346 p.move_left();
2347 p.move_left();
2348 p.move_left();
2349 assert_eq!(p.cursor_chars, 0);
2350 }
2351
2352 #[test]
2353 fn edge_scroll_speed_stops_outside_band() {
2354 assert_eq!(edge_scroll_speed(120.0), 0.0);
2356 assert_eq!(edge_scroll_speed(90.01), 0.0);
2357 assert_eq!(edge_scroll_speed(90.0), 4.0);
2359 assert_eq!(edge_scroll_speed(60.0), 4.0);
2360 assert_eq!(edge_scroll_speed(59.99), 8.0);
2361 assert_eq!(edge_scroll_speed(30.0), 8.0);
2363 assert_eq!(edge_scroll_speed(29.99), 16.0);
2364 assert_eq!(edge_scroll_speed(0.0), 16.0);
2366 assert_eq!(edge_scroll_speed(29.99), 16.0);
2367 }
2368
2369 #[test]
2370 fn edge_scroll_speed_caps_negative_runaway() {
2371 assert_eq!(edge_scroll_speed(-100.0), 16.0);
2373 assert_eq!(edge_scroll_speed(-1000.0), 16.0);
2374 }
2375
2376 #[allow(clippy::expect_used, clippy::unwrap_used)]
2384 #[test]
2385 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2386 fn grid_state_behavior_under_application() {
2387 gpui::Application::new().run(|cx| {
2388 let focus = cx.focus_handle();
2389
2390 let mut state = GridState::new(
2392 GridData::new(
2393 vec![Column::new("n", ColumnKind::Integer, 100.0)],
2394 vec![vec![CellValue::Integer(1)]],
2395 )
2396 .expect("rectangular"),
2397 crate::config::GridConfig::default(),
2398 focus.clone(),
2399 );
2400 let _ = format_current_status(&state);
2401 assert_eq!(state.selection, Selection::None);
2402
2403 state.last_mouse_pos = Some(Point {
2405 x: px(120.0),
2406 y: px(80.0),
2407 });
2408 let s = format_current_status(&state);
2409 assert!(s.contains("(120, 80)"), "missing positional, got: {s}");
2410
2411 let mut state = GridState::new(
2413 GridData::new(
2414 vec![Column::new("name", ColumnKind::Text, 100.0)],
2415 vec![
2416 vec![CellValue::Text("alpha".into())],
2417 vec![CellValue::Text("beeb".into())],
2418 vec![CellValue::Text("gamma".into())],
2419 ],
2420 )
2421 .expect("rectangular"),
2422 crate::config::GridConfig::default(),
2423 focus.clone(),
2424 );
2425 state.filters[0] = ColumnFilter {
2426 predicate: FilterPredicate::Text {
2427 op: TextOp::Contains,
2428 operand: "a".into(),
2429 },
2430 values: None,
2431 };
2432 state.toggle_sort(0);
2433 state.recompute();
2434 assert_eq!(state.display_indices.as_slice(), &[0, 2]);
2435 state.toggle_sort(0);
2436 state.recompute();
2437 assert_eq!(state.display_indices.as_slice(), &[2, 0]);
2438 state.filters[0] = ColumnFilter::default();
2439 state.toggle_sort(0);
2440 state.recompute();
2441 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
2442
2443 let mut state = GridState::new(
2445 GridData::new(
2446 vec![Column::new("v", ColumnKind::Integer, 80.0)],
2447 vec![vec![CellValue::Integer(1)]],
2448 )
2449 .expect("rectangular"),
2450 crate::config::GridConfig::default(),
2451 focus.clone(),
2452 );
2453 state.toggle_sort(0);
2454 assert_eq!(state.sort, Some((0, SortDirection::Ascending)));
2455 state.toggle_sort(0);
2456 assert_eq!(state.sort, Some((0, SortDirection::Descending)));
2457 state.toggle_sort(0);
2458 assert_eq!(state.sort, None);
2459
2460 let mut state = GridState::new(
2462 GridData::new(
2463 vec![
2464 Column::new("a", ColumnKind::Integer, 80.0),
2465 Column::new("b", ColumnKind::Integer, 80.0),
2466 ],
2467 vec![vec![CellValue::Integer(1), CellValue::Integer(2)]],
2468 )
2469 .expect("rectangular"),
2470 crate::config::GridConfig::default(),
2471 focus.clone(),
2472 );
2473 state.select_all();
2474 assert_eq!(state.selection, Selection::CellRange(0, 0, 0, 1));
2475
2476 let mut state = GridState::new(
2478 GridData::new(vec![Column::new("a", ColumnKind::Integer, 80.0)], vec![])
2479 .expect("rectangular"),
2480 crate::config::GridConfig::default(),
2481 focus.clone(),
2482 );
2483 state.select_all();
2484 assert_eq!(state.selection, Selection::None);
2485
2486 let mut state = GridState::new(
2488 GridData::new(
2489 vec![Column::new("v", ColumnKind::Decimal, 100.0)],
2490 vec![vec![CellValue::Decimal(1.234)]],
2491 )
2492 .expect("rectangular"),
2493 crate::config::GridConfig::default(),
2494 focus.clone(),
2495 );
2496 assert_eq!(state.resolved_formats[0].number.decimals, 2);
2497 let mut cfg = crate::config::GridConfig::default();
2498 cfg.column_overrides = vec![crate::config::ColumnOverride {
2499 number: Some(crate::config::NumberFormat {
2500 decimals: 6,
2501 ..Default::default()
2502 }),
2503 ..Default::default()
2504 }];
2505 state.set_config(cfg);
2506 assert_eq!(state.resolved_formats[0].number.decimals, 6);
2507
2508 let mut state = GridState::new(
2510 GridData::new(
2511 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2512 vec![vec![CellValue::Integer(1)]],
2513 )
2514 .expect("rectangular"),
2515 crate::config::GridConfig::default(),
2516 focus.clone(),
2517 );
2518 assert!(!state.wants_edge_scroll_tick());
2519 state.is_dragging = true;
2520 assert!(state.wants_edge_scroll_tick());
2521
2522 cx.quit();
2523 });
2524 }
2525
2526 #[allow(clippy::expect_used, clippy::unwrap_used)]
2527 #[test]
2528 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2529 fn context_menu_request_construction() {
2530 use crate::grid::context_menu::ContextMenuTarget;
2531
2532 gpui::Application::new().run(|cx| {
2533 let focus = cx.focus_handle();
2534
2535 let mut state = GridState::new(
2537 GridData::new(
2538 vec![
2539 Column::new("id", ColumnKind::Integer, 80.0),
2540 Column::new("name", ColumnKind::Text, 100.0),
2541 ],
2542 vec![
2543 vec![CellValue::Integer(1), CellValue::Text("alpha".into())],
2544 vec![CellValue::Integer(2), CellValue::Text("beta".into())],
2545 vec![CellValue::Integer(3), CellValue::Text("gamma".into())],
2546 ],
2547 )
2548 .expect("rectangular"),
2549 crate::config::GridConfig::default(),
2550 focus.clone(),
2551 );
2552 state.sort = Some((0, SortDirection::Descending));
2554 state.recompute();
2555 assert_eq!(state.display_indices.as_slice(), &[2, 1, 0]);
2556
2557 let target = ContextMenuTarget::Cell {
2559 display_row_index: 0,
2560 source_row_index: 2,
2561 column_index: 1,
2562 };
2563 let sel = Selection::Cell(0, 1);
2564 let req = state.build_context_menu_request(target, &sel);
2565 assert_eq!(req.target.column_index(), Some(1));
2566 let cells = req.selected_cells();
2567 assert_eq!(cells.len(), 1);
2568 assert_eq!(cells[0].source_row_index, 2);
2569 assert_eq!(cells[0].column_name, "name");
2570 assert_eq!(cells[0].value, CellValue::Text("gamma".into()));
2571 let rows = req.selected_rows();
2572 assert_eq!(rows.len(), 1);
2573 assert_eq!(rows[0].source_row_index, 2);
2574 assert_eq!(rows[0].value_by_name("id"), Some(&CellValue::Integer(3)));
2575
2576 let target = ContextMenuTarget::Cell {
2578 display_row_index: 0,
2579 source_row_index: 2,
2580 column_index: 0,
2581 };
2582 let sel = Selection::CellRange(0, 0, 1, 1);
2583 let req = state.build_context_menu_request(target, &sel);
2584 assert_eq!(req.selected_cell_count(), 4); let rows = req.selected_rows();
2586 assert_eq!(rows.len(), 2);
2587 assert_eq!(rows[0].source_row_index, 2);
2589 assert_eq!(rows[1].source_row_index, 1);
2590
2591 let target = ContextMenuTarget::RowHeader {
2593 display_row_index: 1,
2594 source_row_index: 1,
2595 };
2596 let sel = Selection::RowRange(0, 2);
2597 let req = state.build_context_menu_request(target, &sel);
2598 let rows = req.selected_rows();
2599 assert_eq!(rows.len(), 3);
2600 assert_eq!(rows[0].values.len(), 2);
2602 assert_eq!(req.selected_cell_count(), 6); let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
2608 let sel = Selection::Column(0);
2609 let req = state.build_context_menu_request(target, &sel);
2610 assert!(req.is_column_oriented());
2611 assert_eq!(req.selected_row_count(), 0);
2612 assert!(req.selected_rows().is_empty());
2613 assert_eq!(req.selected_cells().len(), 3); let empty_state = GridState::new(
2617 GridData::new(vec![Column::new("x", ColumnKind::Integer, 80.0)], vec![])
2618 .expect("rectangular"),
2619 crate::config::GridConfig::default(),
2620 focus.clone(),
2621 );
2622 let target = ContextMenuTarget::Cell {
2623 display_row_index: 0,
2624 source_row_index: 0,
2625 column_index: 0,
2626 };
2627 let req = empty_state.build_context_menu_request(target, &Selection::None);
2628 assert!(req.selected_cells().is_empty());
2629 assert!(req.selected_rows().is_empty());
2630
2631 cx.quit();
2632 });
2633 }
2634
2635 #[allow(clippy::expect_used, clippy::unwrap_used)]
2636 #[test]
2637 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2638 fn effective_selection_for_context_target() {
2639 gpui::Application::new().run(|cx| {
2640 let focus = cx.focus_handle();
2641 let mut state = GridState::new(
2642 GridData::new(
2643 vec![
2644 Column::new("a", ColumnKind::Integer, 80.0),
2645 Column::new("b", ColumnKind::Integer, 80.0),
2646 ],
2647 vec![
2648 vec![CellValue::Integer(1), CellValue::Integer(2)],
2649 vec![CellValue::Integer(3), CellValue::Integer(4)],
2650 ],
2651 )
2652 .expect("rectangular"),
2653 crate::config::GridConfig::default(),
2654 focus,
2655 );
2656
2657 state.selection = Selection::Cell(0, 0);
2659 let target = ContextMenuTarget::Cell {
2660 display_row_index: 1,
2661 source_row_index: 1,
2662 column_index: 1,
2663 };
2664 let eff = state.effective_selection_for_context_target(&target);
2665 assert_eq!(eff, Selection::Cell(1, 1));
2666
2667 state.selection = Selection::CellRange(0, 0, 1, 1);
2669 let target = ContextMenuTarget::Cell {
2670 display_row_index: 1,
2671 source_row_index: 1,
2672 column_index: 1,
2673 };
2674 let eff = state.effective_selection_for_context_target(&target);
2675 assert_eq!(eff, Selection::CellRange(0, 0, 1, 1));
2676
2677 state.selection = Selection::Cell(0, 0);
2679 let target = ContextMenuTarget::RowHeader {
2680 display_row_index: 1,
2681 source_row_index: 1,
2682 };
2683 let eff = state.effective_selection_for_context_target(&target);
2684 assert_eq!(eff, Selection::Row(1));
2685
2686 state.selection = Selection::RowRange(0, 1);
2688 let target = ContextMenuTarget::RowHeader {
2689 display_row_index: 1,
2690 source_row_index: 1,
2691 };
2692 let eff = state.effective_selection_for_context_target(&target);
2693 assert_eq!(eff, Selection::RowRange(0, 1));
2694
2695 state.selection = Selection::Cell(1, 1);
2697 let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
2698 let eff = state.effective_selection_for_context_target(&target);
2699 assert_eq!(eff, Selection::Cell(1, 1));
2700
2701 cx.quit();
2702 });
2703 }
2704
2705 #[allow(clippy::expect_used, clippy::unwrap_used)]
2706 #[test]
2707 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2708 fn context_menu_target_from_hit_maps_correctly() {
2709 gpui::Application::new().run(|cx| {
2710 let focus = cx.focus_handle();
2711 let state = GridState::new(
2712 GridData::new(
2713 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2714 vec![vec![CellValue::Integer(1)], vec![CellValue::Integer(2)]],
2715 )
2716 .expect("rectangular"),
2717 crate::config::GridConfig::default(),
2718 focus,
2719 );
2720
2721 let t = state
2723 .context_menu_target_from_hit(HitResult::Cell(1, 0))
2724 .unwrap();
2725 assert_eq!(
2726 t,
2727 ContextMenuTarget::Cell {
2728 display_row_index: 1,
2729 source_row_index: 1,
2730 column_index: 0,
2731 }
2732 );
2733
2734 let t = state
2736 .context_menu_target_from_hit(HitResult::RowHeader(0))
2737 .unwrap();
2738 assert_eq!(
2739 t,
2740 ContextMenuTarget::RowHeader {
2741 display_row_index: 0,
2742 source_row_index: 0,
2743 }
2744 );
2745
2746 let t = state
2748 .context_menu_target_from_hit(HitResult::ColumnHeader(0))
2749 .unwrap();
2750 assert_eq!(t, ContextMenuTarget::ColumnHeader { column_index: 0 });
2751
2752 let t = state
2754 .context_menu_target_from_hit(HitResult::SortButton(0))
2755 .unwrap();
2756 assert_eq!(t, ContextMenuTarget::SortButton { column_index: 0 });
2757
2758 assert!(state
2760 .context_menu_target_from_hit(HitResult::VerticalScrollbar)
2761 .is_none());
2762 assert!(state
2763 .context_menu_target_from_hit(HitResult::None)
2764 .is_none());
2765
2766 cx.quit();
2767 });
2768 }
2769
2770 #[allow(clippy::expect_used, clippy::unwrap_used)]
2771 #[test]
2772 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2773 fn convert_context_menu_items_maps_variants() {
2774 use crate::grid::context_menu::ContextMenuItem;
2775
2776 let items = vec![
2777 ContextMenuItem::BuiltIn(MenuAction::SortAscending),
2778 ContextMenuItem::action("copy", "Copy value"),
2779 ContextMenuItem::separator(),
2780 ];
2781 let internal = GridState::convert_context_menu_items(items);
2782 assert!(matches!(
2783 internal[0],
2784 MenuItem::Action(MenuAction::SortAscending)
2785 ));
2786 assert!(
2787 matches!(&internal[1], MenuItem::Custom { id, label } if id == "copy" && label == "Copy value")
2788 );
2789 assert!(matches!(internal[2], MenuItem::Separator));
2790 }
2791
2792 #[allow(clippy::expect_used, clippy::unwrap_used)]
2793 #[test]
2794 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2795 fn execute_custom_context_menu_action_invokes_provider() {
2796 use crate::grid::context_menu::{
2797 ContextMenuProvider, ContextMenuProviderHandle, ContextMenuRequest,
2798 };
2799 use std::sync::{Arc, Mutex};
2800
2801 #[derive(Default)]
2802 struct TestProvider {
2803 last_action: Arc<Mutex<Option<String>>>,
2804 }
2805 impl ContextMenuProvider for TestProvider {
2806 fn menu_items(&self, _request: &ContextMenuRequest) -> Vec<ContextMenuItem> {
2807 vec![ContextMenuItem::action("test", "Test")]
2808 }
2809 fn on_action(
2810 &self,
2811 action_id: &str,
2812 _request: &ContextMenuRequest,
2813 _state: &mut GridState,
2814 _cx: &mut gpui::App,
2815 ) {
2816 *self.last_action.lock().unwrap() = Some(action_id.to_string());
2817 }
2818 }
2819
2820 gpui::Application::new().run(|cx| {
2821 let focus = cx.focus_handle();
2822 let mut state = GridState::new(
2823 GridData::new(
2824 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2825 vec![vec![CellValue::Integer(1)]],
2826 )
2827 .expect("rectangular"),
2828 crate::config::GridConfig::default(),
2829 focus,
2830 );
2831
2832 let last = Arc::new(Mutex::new(None));
2833 state.context_menu_provider = Some(ContextMenuProviderHandle::new(TestProvider {
2834 last_action: last.clone(),
2835 }));
2836
2837 let target = ContextMenuTarget::Cell {
2838 display_row_index: 0,
2839 source_row_index: 0,
2840 column_index: 0,
2841 };
2842 let request = state.build_context_menu_request(target, &Selection::Cell(0, 0));
2843 state.execute_custom_context_menu_action(
2844 PendingCustomContextMenuAction {
2845 id: "test".into(),
2846 request,
2847 },
2848 cx,
2849 );
2850 assert_eq!(*last.lock().unwrap(), Some("test".to_string()));
2851 assert!(state.context_menu.is_none());
2852
2853 cx.quit();
2854 });
2855 }
2856
2857 #[test]
2858 fn filter_panel_to_filter_with_all_checked_has_no_value_set() {
2859 let panel = FilterPanel {
2860 col: 0,
2861 anchor: Point {
2862 x: px(0.0),
2863 y: px(0.0),
2864 },
2865 kind: ColumnKind::Text,
2866 search: TextInput::default(),
2867 op_index: 0,
2868 op_menu_open: false,
2869 operand_a: TextInput::default(),
2870 operand_b: TextInput::default(),
2871 focus: FilterInput::Search,
2872 auto_apply: true,
2873 distinct: vec![
2874 FilterValueRow {
2875 label: "alpha".into(),
2876 checked: true,
2877 },
2878 FilterValueRow {
2879 label: "beta".into(),
2880 checked: true,
2881 },
2882 ],
2883 };
2884 let f = panel.to_filter();
2885 assert!(f.values.is_none(), "all checked => no value allow-list");
2886 assert!(
2887 !f.is_active(),
2888 "default predicate + all checked => inactive"
2889 );
2890 }
2891
2892 #[test]
2893 fn filter_panel_to_filter_with_unchecked_value_builds_allow_set() {
2894 let panel = FilterPanel {
2895 col: 0,
2896 anchor: Point {
2897 x: px(0.0),
2898 y: px(0.0),
2899 },
2900 kind: ColumnKind::Text,
2901 search: TextInput::default(),
2902 op_index: 0,
2903 op_menu_open: false,
2904 operand_a: TextInput::default(),
2905 operand_b: TextInput::default(),
2906 focus: FilterInput::Search,
2907 auto_apply: true,
2908 distinct: vec![
2909 FilterValueRow {
2910 label: "alpha".into(),
2911 checked: true,
2912 },
2913 FilterValueRow {
2914 label: "beta".into(),
2915 checked: false,
2916 },
2917 ],
2918 };
2919 let f = panel.to_filter();
2920 assert!(f.is_active(), "unchecked value => active filter");
2921 let set = f.values.expect("should have a value set");
2922 assert!(set.contains("alpha"));
2923 assert!(!set.contains("beta"));
2924 }
2925
2926 #[test]
2927 fn filter_panel_visible_indices_respects_search() {
2928 let panel = FilterPanel {
2929 col: 0,
2930 anchor: Point {
2931 x: px(0.0),
2932 y: px(0.0),
2933 },
2934 kind: ColumnKind::Text,
2935 search: TextInput::new("al".into()),
2936 op_index: 0,
2937 op_menu_open: false,
2938 operand_a: TextInput::default(),
2939 operand_b: TextInput::default(),
2940 focus: FilterInput::Search,
2941 auto_apply: true,
2942 distinct: vec![
2943 FilterValueRow {
2944 label: "alpha".into(),
2945 checked: true,
2946 },
2947 FilterValueRow {
2948 label: "beta".into(),
2949 checked: true,
2950 },
2951 FilterValueRow {
2952 label: "gamma".into(),
2953 checked: true,
2954 },
2955 ],
2956 };
2957 let vis = panel.visible_indices();
2958 assert_eq!(vis, vec![0], "search 'al' matches only alpha");
2959 }
2960
2961 #[test]
2962 fn filter_panel_all_checked_ignores_search() {
2963 let mut panel = FilterPanel {
2964 col: 0,
2965 anchor: Point {
2966 x: px(0.0),
2967 y: px(0.0),
2968 },
2969 kind: ColumnKind::Text,
2970 search: TextInput::new("al".into()),
2971 op_index: 0,
2972 op_menu_open: false,
2973 operand_a: TextInput::default(),
2974 operand_b: TextInput::default(),
2975 focus: FilterInput::Search,
2976 auto_apply: true,
2977 distinct: vec![
2978 FilterValueRow {
2979 label: "alpha".into(),
2980 checked: true,
2981 },
2982 FilterValueRow {
2983 label: "beta".into(),
2984 checked: false,
2985 },
2986 FilterValueRow {
2987 label: "gamma".into(),
2988 checked: true,
2989 },
2990 ],
2991 };
2992 assert!(
2995 !panel.all_checked(),
2996 "beta is unchecked, so not all values are checked (search is irrelevant)"
2997 );
2998
2999 panel.search = TextInput::new("zzz".into());
3001 for row in &mut panel.distinct {
3002 row.checked = true;
3003 }
3004 assert!(
3005 panel.all_checked(),
3006 "all values checked -> Select All stays checked regardless of empty search"
3007 );
3008 }
3009
3010 #[allow(clippy::expect_used, clippy::unwrap_used)]
3011 #[test]
3012 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3013 fn filter_panel_open_apply_clear_state_flow() {
3014 gpui::Application::new().run(|cx| {
3015 let focus = cx.focus_handle();
3016 let mut state = GridState::new(
3017 GridData::new(
3018 vec![Column::new("name", ColumnKind::Text, 100.0)],
3019 vec![
3020 vec![CellValue::Text("alpha".into())],
3021 vec![CellValue::Text("beta".into())],
3022 vec![CellValue::Text("gamma".into())],
3023 ],
3024 )
3025 .expect("rectangular"),
3026 crate::config::GridConfig::default(),
3027 focus,
3028 );
3029
3030 let anchor = Point {
3032 x: px(50.0),
3033 y: px(20.0),
3034 };
3035 state.open_filter_panel(0, Some(anchor));
3036 let panel = state.filter_panel.as_ref().expect("panel should be open");
3037 assert_eq!(panel.col, 0);
3038 assert_eq!(panel.anchor, anchor);
3039 assert_eq!(panel.distinct.len(), 3);
3040 assert!(
3041 panel.distinct.iter().all(|r| r.checked),
3042 "all checked by default"
3043 );
3044 assert!(panel.auto_apply, "auto_apply defaults to true");
3045 assert_eq!(panel.kind, ColumnKind::Text);
3046
3047 state.toggle_filter_value(1);
3049 state.apply_filter_panel();
3050 assert_eq!(
3051 state.display_indices.as_slice(),
3052 &[0, 2],
3053 "beta should be filtered out"
3054 );
3055
3056 state.clear_filter_panel();
3058 assert_eq!(
3059 state.display_indices.as_slice(),
3060 &[0, 1, 2],
3061 "all rows visible after clear"
3062 );
3063 assert!(
3064 state.filters[0] == ColumnFilter::default(),
3065 "filter reset to default"
3066 );
3067
3068 state.open_filter_panel(0, Some(anchor));
3070 let panel = state.filter_panel.as_mut().expect("panel open");
3071 panel.op_index = 1; panel.operand_a = TextInput::new("a".into());
3073 state.apply_filter_panel();
3074 assert_eq!(
3075 state.display_indices.as_slice(),
3076 &[0, 2],
3077 "contains 'a' matches alpha and gamma"
3078 );
3079
3080 state.clear_filter_panel();
3082 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
3083
3084 cx.quit();
3085 });
3086 }
3087}