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 pub(crate) fn clamp_scroll_to_bounds(&mut self) {
1002 let (mx, my) = self.max_scroll();
1003 let s = self.scroll_handle.offset();
1004 let nx = f32::from(s.x).clamp(0.0, mx);
1005 let ny = f32::from(s.y).clamp(0.0, my);
1006 if nx != f32::from(s.x) || ny != f32::from(s.y) {
1007 self.scroll_handle.set_offset(Point {
1008 x: px(nx),
1009 y: px(ny),
1010 });
1011 }
1012 }
1013
1014 fn scrollbar_reserved(&self) -> (f32, f32) {
1015 let (cw, ch) = self.content_size();
1016 let vw: f32 = self.bounds.size.width.into();
1017 let vh: f32 = self.bounds.size.height.into();
1018 let vw = vw - self.row_header_width;
1019 let vh = vh - self.header_height;
1020 let reserved_w = if ch > vh { SCROLLBAR_SIZE } else { 0.0 };
1021 let reserved_h = if cw > vw { SCROLLBAR_SIZE } else { 0.0 };
1022 (reserved_w, reserved_h)
1023 }
1024
1025 fn vbar_geom(&self) -> Option<(f32, f32, f32, f32, f32)> {
1026 let (_, ch) = self.content_size();
1027 let (_, rh) = self.scrollbar_reserved();
1028 let vh: f32 = self.bounds.size.height.into();
1029 let vh = vh - self.header_height - rh;
1030 if ch <= vh {
1031 return None;
1032 }
1033 let sw: f32 = self.bounds.size.width.into();
1036 let sh: f32 = self.bounds.size.height.into();
1037 let track_x = sw - SCROLLBAR_SIZE;
1038 let track_y = self.header_height;
1039 let track_h = sh - self.header_height - rh;
1040 let thumb_h = ((track_h * (vh / ch)).max(20.0)).min(track_h);
1041 Some((track_x, track_y, SCROLLBAR_SIZE, track_h, thumb_h))
1042 }
1043
1044 fn hbar_geom(&self) -> Option<(f32, f32, f32, f32, f32)> {
1045 let (cw, _) = self.content_size();
1046 let (rw, _) = self.scrollbar_reserved();
1047 let vw: f32 = self.bounds.size.width.into();
1048 let vw = vw - self.row_header_width - rw;
1049 if cw <= vw {
1050 return None;
1051 }
1052 let sw: f32 = self.bounds.size.width.into();
1055 let sh: f32 = self.bounds.size.height.into();
1056 let track_x = self.row_header_width;
1057 let track_y = sh - SCROLLBAR_SIZE;
1058 let track_w = sw - self.row_header_width - rw;
1059 let thumb_w = ((track_w * (vw / cw)).max(20.0)).min(track_w);
1060 Some((track_x, track_y, track_w, SCROLLBAR_SIZE, thumb_w))
1061 }
1062
1063 pub(crate) fn scroll_to_vbar(&mut self, mouse_y: f32) {
1064 if let Some((_, track_y, _, track_h, thumb_h)) = self.vbar_geom() {
1065 let (_, max_y) = self.max_scroll();
1066 let range = (track_h - thumb_h).max(0.0);
1067 let rel = (mouse_y - track_y - thumb_h * 0.5).clamp(0.0, range);
1068 let frac = if range > 0.0 { rel / range } else { 0.0 };
1069 let new_y = frac * max_y;
1070 let x = self.scroll_handle.offset().x;
1071 self.scroll_handle.set_offset(Point { x, y: px(new_y) });
1072 }
1073 }
1074
1075 pub(crate) fn scroll_to_hbar(&mut self, mouse_x: f32) {
1076 if let Some((track_x, _, track_w, _, thumb_w)) = self.hbar_geom() {
1077 let (max_x, _) = self.max_scroll();
1078 let range = (track_w - thumb_w).max(0.0);
1079 let rel = (mouse_x - track_x - thumb_w * 0.5).clamp(0.0, range);
1080 let frac = if range > 0.0 { rel / range } else { 0.0 };
1081 let new_x = frac * max_x;
1082 let y = self.scroll_handle.offset().y;
1083 self.scroll_handle.set_offset(Point { x: px(new_x), y });
1084 }
1085 }
1086
1087 pub(crate) fn scroll_one_edge_tick(&mut self, dx: f32, dy: f32) {
1088 let (mx, my) = self.max_scroll();
1089 let s = self.scroll_handle.offset();
1090 let new_x: f32 = (f32::from(s.x) + dx).clamp(0.0, mx);
1091 let new_y: f32 = (f32::from(s.y) + dy).clamp(0.0, my);
1092 self.scroll_handle.set_offset(Point {
1093 x: px(new_x),
1094 y: px(new_y),
1095 });
1096 }
1097
1098 pub fn toggle_sort(&mut self, col: usize) {
1099 if self.window.is_some() {
1102 return;
1103 }
1104 self.sort = match self.sort {
1105 Some((c, SortDirection::Ascending)) if c == col => {
1106 Some((col, SortDirection::Descending))
1107 }
1108 Some((c, SortDirection::Descending)) if c == col => None,
1109 _ => Some((col, SortDirection::Ascending)),
1110 };
1111 self.recompute();
1112 }
1113
1114 pub fn handle_mouse_down(&mut self, pos: Point<Pixels>, shift: bool) {
1115 self.handle_mouse_down_with_modifiers(pos, shift, false);
1116 }
1117
1118 pub fn handle_mouse_down_with_modifiers(&mut self, pos: Point<Pixels>, shift: bool, cmd: bool) {
1119 let hit = self.hit_test(pos);
1120 self.click_pos = Some(pos);
1121 self.click_hit = Some(hit);
1122 match hit {
1123 HitResult::VerticalScrollbar => {
1124 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1125 self.scroll_to_vbar(f32::from(pos.y));
1126 self.clear_drag();
1127 }
1128 HitResult::HorizontalScrollbar => {
1129 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1130 self.scroll_to_hbar(f32::from(pos.x));
1131 self.clear_drag();
1132 }
1133 HitResult::ColumnBorder(col) => {
1134 self.resizing_col = Some(col);
1135 self.resize_start_x = f32::from(pos.x);
1136 self.resize_start_width = self.data.columns[col].width;
1137 self.clear_drag();
1138 }
1139 HitResult::ColumnHeader(col) => {
1140 if cmd {
1141 let mut cols: Vec<usize> = match &self.selection {
1144 Selection::Column(c) => vec![*c],
1145 Selection::Columns(cs) => cs.clone(),
1146 _ => Vec::new(),
1147 };
1148 if let Some(idx) = cols.iter().position(|&c| c == col) {
1149 cols.remove(idx);
1150 } else {
1151 cols.push(col);
1152 cols.sort_unstable();
1153 }
1154 self.selection = match cols.len() {
1155 0 => Selection::None,
1156 1 => Selection::Column(cols[0]),
1157 _ => Selection::Columns(cols),
1158 };
1159 self.clear_drag();
1160 } else {
1161 self.selection = Selection::Column(col);
1162 self.start_drag(pos);
1164 self.drag_start_hit = Some(HitResult::ColumnHeader(col));
1165 }
1166 }
1167 HitResult::SortButton(col) => {
1168 self.toggle_sort(col);
1171 self.clear_drag();
1172 }
1173 HitResult::ContextMenuItem(_) => {}
1174 HitResult::RowHeader(row) => {
1175 if cmd {
1176 let mut rows: Vec<usize> = match &self.selection {
1179 Selection::Row(r) => vec![*r],
1180 Selection::RowRange(r1, r2) => (*r1.min(r2)..=*r1.max(r2)).collect(),
1181 Selection::Rows(rs) => rs.clone(),
1182 _ => Vec::new(),
1183 };
1184 if let Some(idx) = rows.iter().position(|&r| r == row) {
1185 rows.remove(idx);
1186 } else {
1187 rows.push(row);
1188 rows.sort_unstable();
1189 }
1190 self.selection = match rows.len() {
1191 0 => Selection::None,
1192 1 => Selection::Row(rows[0]),
1193 _ => Selection::Rows(rows),
1194 };
1195 self.clear_drag();
1196 return;
1197 }
1198 self.selection = if shift {
1199 if let Selection::Row(prev) = self.selection {
1200 let (s, e) = (prev, row);
1201 Selection::RowRange(s.min(e), s.max(e))
1202 } else {
1203 Selection::Row(row)
1204 }
1205 } else {
1206 Selection::Row(row)
1207 };
1208 self.start_drag(pos);
1209 self.drag_start_hit = Some(HitResult::RowHeader(row));
1210 }
1211 HitResult::Cell(row, col) => {
1212 if cmd {
1213 let mut cells: Vec<(usize, usize)> = match &self.selection {
1216 Selection::Cell(r, c) => vec![(*r, *c)],
1217 Selection::Cells(cs) => cs.clone(),
1218 _ => Vec::new(),
1219 };
1220 if let Some(idx) = cells.iter().position(|&rc| rc == (row, col)) {
1221 cells.remove(idx);
1222 } else {
1223 cells.push((row, col));
1224 cells.sort_unstable();
1225 }
1226 self.selection = match cells.len() {
1227 0 => Selection::None,
1228 1 => Selection::Cell(cells[0].0, cells[0].1),
1229 _ => Selection::Cells(cells),
1230 };
1231 self.range_anchor = None;
1232 self.range_active = None;
1233 self.clear_drag();
1234 return;
1235 }
1236 self.selection = if shift {
1237 let anchor = self
1239 .range_anchor
1240 .or(match self.selection {
1241 Selection::Cell(pr, pc) => Some((pr, pc)),
1242 _ => None,
1243 })
1244 .unwrap_or((row, col));
1245 self.range_anchor = Some(anchor);
1246 self.range_active = Some((row, col));
1247 Selection::CellRange(
1248 anchor.0.min(row),
1249 anchor.1.min(col),
1250 anchor.0.max(row),
1251 anchor.1.max(col),
1252 )
1253 } else {
1254 self.range_anchor = Some((row, col));
1255 self.range_active = Some((row, col));
1256 Selection::Cell(row, col)
1257 };
1258 self.start_drag(pos);
1259 self.drag_start_hit = Some(HitResult::Cell(row, col));
1260 }
1261 HitResult::Corner | HitResult::None => {
1262 self.selection = Selection::None;
1263 self.range_anchor = None;
1264 self.range_active = None;
1265 self.context_menu = None;
1266 self.filter_panel = None;
1267 self.clear_drag();
1268 }
1269 }
1270 }
1271
1272 fn start_drag(&mut self, pos: Point<Pixels>) {
1273 self.is_dragging = false;
1274 self.drag_start = Some(pos);
1275 self.scroll_at_click = Some(self.scroll_handle.offset());
1276 self.last_mouse_pos = Some(pos);
1277 }
1278
1279 pub(crate) fn open_context_menu(&mut self, col: usize, anchor: Point<Pixels>) {
1280 self.context_menu = Some(menu_mod::ContextMenu::standard(col, anchor));
1281 self.filter_panel = None;
1282 }
1283
1284 pub(crate) fn context_menu_target_from_hit(&self, hit: HitResult) -> Option<ContextMenuTarget> {
1287 match hit {
1288 HitResult::Cell(row, col) => {
1289 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1290 Some(ContextMenuTarget::Cell {
1291 display_row_index: row,
1292 source_row_index: source_row,
1293 column_index: col,
1294 })
1295 }
1296 HitResult::RowHeader(row) => {
1297 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1298 Some(ContextMenuTarget::RowHeader {
1299 display_row_index: row,
1300 source_row_index: source_row,
1301 })
1302 }
1303 HitResult::ColumnHeader(col) => {
1304 Some(ContextMenuTarget::ColumnHeader { column_index: col })
1305 }
1306 HitResult::SortButton(col) => Some(ContextMenuTarget::SortButton { column_index: col }),
1307 _ => None,
1308 }
1309 }
1310
1311 pub(crate) fn effective_selection_for_context_target(
1316 &self,
1317 target: &ContextMenuTarget,
1318 ) -> Selection {
1319 match target {
1320 ContextMenuTarget::Cell {
1321 display_row_index,
1322 column_index,
1323 ..
1324 } => {
1325 if is_cell_selected(&self.selection, *display_row_index, *column_index) {
1326 self.selection.clone()
1327 } else {
1328 Selection::Cell(*display_row_index, *column_index)
1329 }
1330 }
1331 ContextMenuTarget::RowHeader {
1332 display_row_index, ..
1333 } => {
1334 if is_row_selected(&self.selection, *display_row_index) {
1335 self.selection.clone()
1336 } else {
1337 Selection::Row(*display_row_index)
1338 }
1339 }
1340 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. } => {
1341 self.selection.clone()
1342 }
1343 }
1344 }
1345
1346 pub(crate) fn build_context_menu_request(
1358 &self,
1359 target: ContextMenuTarget,
1360 selection: &Selection,
1361 ) -> ContextMenuRequest {
1362 let mut target = target;
1369 let mut selection = selection.clone();
1370 if let Some(w) = self.window {
1371 let resident_last = self.data.rows.len().saturating_sub(1);
1372 let to_resident = |dr: usize| dr.saturating_sub(w.offset).min(resident_last);
1373 target = match target {
1374 ContextMenuTarget::Cell {
1375 display_row_index,
1376 source_row_index,
1377 column_index,
1378 } => ContextMenuTarget::Cell {
1379 display_row_index: to_resident(display_row_index),
1380 source_row_index,
1381 column_index,
1382 },
1383 ContextMenuTarget::RowHeader {
1384 display_row_index,
1385 source_row_index,
1386 } => ContextMenuTarget::RowHeader {
1387 display_row_index: to_resident(display_row_index),
1388 source_row_index,
1389 },
1390 other => other,
1391 };
1392 selection = match selection {
1393 Selection::Cell(r, c) => Selection::Cell(to_resident(r), c),
1394 Selection::Row(r) => Selection::Row(to_resident(r)),
1395 Selection::CellRange(r1, c1, r2, c2) => {
1396 Selection::CellRange(to_resident(r1), c1, to_resident(r2), c2)
1397 }
1398 other => other,
1399 };
1400 }
1401 let selection = &selection;
1402
1403 let nrows = self.display_indices.len();
1404 let ncols = self.data.columns.len();
1405
1406 let (r1, c1, r2, c2) = match selection.normalized_bounds() {
1407 Some((r1, c1, r2, c2)) => {
1408 let r1 = r1.min(nrows.saturating_sub(1));
1409 let r2 = r2.min(nrows.saturating_sub(1));
1410 let c1 = c1.min(ncols.saturating_sub(1));
1411 let c2 = c2.min(ncols.saturating_sub(1));
1412 (r1, c1, r2, c2)
1413 }
1414 None => match &target {
1415 ContextMenuTarget::Cell {
1416 display_row_index,
1417 column_index,
1418 ..
1419 } => (
1420 *display_row_index,
1421 *column_index,
1422 *display_row_index,
1423 *column_index,
1424 ),
1425 ContextMenuTarget::RowHeader {
1426 display_row_index, ..
1427 } => (
1428 *display_row_index,
1429 0,
1430 *display_row_index,
1431 ncols.saturating_sub(1),
1432 ),
1433 ContextMenuTarget::ColumnHeader { column_index }
1434 | ContextMenuTarget::SortButton { column_index } => {
1435 (0, *column_index, nrows.saturating_sub(1), *column_index)
1436 }
1437 },
1438 };
1439
1440 let menu_selection = ContextMenuSelection {
1441 row_start: r1,
1442 row_end: r2,
1443 column_start: c1,
1444 column_end: c2,
1445 };
1446
1447 let column_oriented =
1452 matches!(
1453 target,
1454 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. }
1455 ) || matches!(selection, Selection::Column(_) | Selection::Columns(_));
1456
1457 ContextMenuRequest::new(
1458 target,
1459 Some(menu_selection),
1460 Arc::clone(&self.data_rows),
1461 Arc::clone(&self.display_indices),
1462 Arc::clone(&self.column_meta),
1463 column_oriented,
1464 )
1465 }
1466
1467 pub(crate) fn execute_custom_context_menu_action(
1471 &mut self,
1472 pending: PendingCustomContextMenuAction,
1473 cx: &mut App,
1474 ) {
1475 self.context_menu = None;
1476 self.filter_panel = None;
1477
1478 let Some(provider) = self.context_menu_provider.clone() else {
1479 return;
1480 };
1481
1482 provider.on_action(&pending.id, &pending.request, self, cx);
1483 }
1484
1485 pub(crate) fn convert_context_menu_items(items: Vec<ContextMenuItem>) -> Vec<MenuItem> {
1488 items
1489 .into_iter()
1490 .map(|item| match item {
1491 ContextMenuItem::BuiltIn(action) => MenuItem::Action(action),
1492 ContextMenuItem::Action { id, label } => MenuItem::Custom { id, label },
1493 ContextMenuItem::Separator => MenuItem::Separator,
1494 })
1495 .collect()
1496 }
1497
1498 pub fn execute_action(&mut self, action: MenuAction, col: usize, cx: &mut App) {
1499 match action {
1500 MenuAction::SelectColumn => {
1501 self.selection = Selection::Column(col);
1502 }
1503 MenuAction::CopyColumn => {
1504 let text = self.column_text(col);
1505 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1506 }
1507 MenuAction::CopyColumnWithHeaders => {
1508 let mut text = String::new();
1509 text.push_str(&self.data.columns[col].name);
1510 text.push('\n');
1511 text.push_str(&self.column_text(col));
1512 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1513 }
1514 MenuAction::SortAscending => {
1515 self.sort = Some((col, SortDirection::Ascending));
1516 self.recompute();
1517 }
1518 MenuAction::SortDescending => {
1519 self.sort = Some((col, SortDirection::Descending));
1520 self.recompute();
1521 }
1522 MenuAction::ClearSort => {
1523 self.sort = None;
1524 self.recompute();
1525 }
1526 MenuAction::FilterPrompt => {
1527 let anchor = self.context_menu.as_ref().map(|m| m.anchor);
1528 self.open_filter_panel(col, anchor);
1529 }
1530 MenuAction::ClearFilter => {
1531 if col < self.filters.len() {
1532 self.filters[col] = ColumnFilter::default();
1533 self.recompute();
1534 }
1535 }
1536 }
1537 self.context_menu = None;
1538 }
1539
1540 pub fn open_filter_panel(&mut self, col: usize, _anchor: Option<Point<Pixels>>) {
1551 if col >= self.data.columns.len() {
1552 return;
1553 }
1554 let sx = f32::from(self.scroll_handle.offset().x);
1555 let col_x = self.row_header_width
1556 + self.data.columns[..col]
1557 .iter()
1558 .map(|c| c.width)
1559 .sum::<f32>()
1560 - sx;
1561 let anchor = Point {
1562 x: px(col_x + self.data.columns[col].width * 0.5),
1563 y: px(0.0),
1564 };
1565 let kind = self.data.columns[col].kind;
1566 let existing = self.filters.get(col).cloned().unwrap_or_default();
1567
1568 let distinct = {
1570 let fmt = &self.resolved_formats[col];
1571 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1572 let mut pairs: Vec<(String, &CellValue)> = Vec::new();
1573 for row in &self.data.rows {
1574 let cell = &row[col];
1575 let (label, _) = format_cell(cell, fmt);
1576 if seen.insert(label.clone()) {
1577 pairs.push((label, cell));
1578 }
1579 }
1580 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
1581 pairs
1582 .into_iter()
1583 .map(|(label, _)| {
1584 let checked = match &existing.values {
1585 None => true,
1586 Some(set) => set.contains(&label),
1587 };
1588 FilterValueRow { label, checked }
1589 })
1590 .collect()
1591 };
1592
1593 let (op_index, operand_a, operand_b) = seed_operator(kind, &existing.predicate);
1594
1595 self.context_menu = None;
1596 self.filter_panel = Some(FilterPanel {
1597 col,
1598 anchor,
1599 kind,
1600 search: TextInput::default(),
1601 op_index,
1602 op_menu_open: false,
1603 operand_a: TextInput::new(operand_a),
1604 operand_b: TextInput::new(operand_b),
1605 focus: FilterInput::Search,
1606 auto_apply: true,
1607 distinct,
1608 });
1609 }
1610
1611 pub fn apply_filter_panel(&mut self) {
1614 let Some(panel) = &self.filter_panel else {
1615 return;
1616 };
1617 let col = panel.col;
1618 let filter = panel.to_filter();
1619 if col < self.filters.len() {
1620 self.filters[col] = filter;
1621 self.recompute();
1622 }
1623 }
1624
1625 pub fn maybe_auto_apply(&mut self) {
1627 if self.filter_panel.is_some() {
1628 self.apply_filter_panel();
1629 }
1630 }
1631
1632 pub fn clear_filter_panel(&mut self) {
1635 let mut target_col = None;
1636 if let Some(panel) = &mut self.filter_panel {
1637 panel.op_index = 0;
1638 panel.op_menu_open = false;
1639 panel.operand_a = TextInput::default();
1640 panel.operand_b = TextInput::default();
1641 panel.search = TextInput::default();
1642 for row in &mut panel.distinct {
1643 row.checked = true;
1644 }
1645 target_col = Some(panel.col);
1646 }
1647 if let Some(col) = target_col {
1648 if col < self.filters.len() {
1649 self.filters[col] = ColumnFilter::default();
1650 }
1651 }
1652 self.recompute();
1653 }
1654
1655 pub fn set_panel_sort(&mut self, direction: SortDirection) {
1658 if let Some(panel) = &self.filter_panel {
1659 let col = panel.col;
1660 self.sort = match self.sort {
1661 Some((c, d)) if c == col && d == direction => None,
1662 _ => Some((col, direction)),
1663 };
1664 self.recompute();
1665 }
1666 }
1667
1668 pub fn toggle_filter_value(&mut self, index: usize) {
1671 if let Some(panel) = &mut self.filter_panel {
1672 if let Some(row) = panel.distinct.get_mut(index) {
1673 row.checked = !row.checked;
1674 }
1675 }
1676 self.maybe_auto_apply();
1677 }
1678
1679 pub fn toggle_filter_select_all(&mut self) {
1684 if let Some(panel) = &mut self.filter_panel {
1685 let target = !panel.all_checked();
1686 for row in &mut panel.distinct {
1687 row.checked = target;
1688 }
1689 }
1690 self.maybe_auto_apply();
1691 }
1692
1693 pub fn set_filter_operator(&mut self, op_index: usize) {
1696 if let Some(panel) = &mut self.filter_panel {
1697 panel.op_index = op_index;
1698 panel.op_menu_open = false;
1699 if op_index != 0 {
1700 panel.focus = FilterInput::OperandA;
1701 }
1702 }
1703 self.maybe_auto_apply();
1704 }
1705
1706 pub fn toggle_filter_op_menu(&mut self) {
1708 if let Some(panel) = &mut self.filter_panel {
1709 panel.op_menu_open = !panel.op_menu_open;
1710 }
1711 }
1712
1713 pub fn set_filter_focus(&mut self, focus: FilterInput) {
1715 if let Some(panel) = &mut self.filter_panel {
1716 panel.focus = focus;
1717 }
1718 }
1719
1720 pub fn toggle_filter_auto_apply(&mut self) {
1722 if let Some(panel) = &mut self.filter_panel {
1723 panel.auto_apply = !panel.auto_apply;
1724 }
1725 self.maybe_auto_apply();
1726 }
1727
1728 fn column_text(&self, col: usize) -> String {
1729 let mut text = String::new();
1730 let fmt = &self.resolved_formats[col];
1731 for &row_idx in self.display_indices.iter() {
1732 let cell = &self.data.rows[row_idx][col];
1733 let (s, _) = format_cell(cell, fmt);
1734 text.push_str(&s);
1735 text.push('\n');
1736 }
1737 text
1738 }
1739
1740 fn clear_drag(&mut self) {
1741 self.is_dragging = false;
1742 self.drag_start = None;
1743 self.drag_start_hit = None;
1744 self.scroll_at_click = None;
1745 }
1746
1747 fn drag_world_corners(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
1748 let start = self.drag_start?;
1749 let mouse = self.last_mouse_pos?;
1750 let click_scroll = self
1751 .scroll_at_click
1752 .unwrap_or_else(|| self.scroll_handle.offset());
1753 let scroll = self.scroll_handle.offset();
1754 let sx_click: f32 = click_scroll.x.into();
1755 let sy_click: f32 = click_scroll.y.into();
1756 let sx: f32 = scroll.x.into();
1757 let sy: f32 = scroll.y.into();
1758 let sx0: f32 = start.x.into();
1759 let sy0: f32 = start.y.into();
1760 let mx: f32 = mouse.x.into();
1761 let my: f32 = mouse.y.into();
1762 let start_world = Point {
1763 x: px(sx0 + sx_click),
1764 y: px(sy0 + sy_click),
1765 };
1766 let end_world = Point {
1767 x: px(mx + sx),
1768 y: px(my + sy),
1769 };
1770 Some((start_world, end_world))
1771 }
1772
1773 pub fn drag_screen_rect(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
1774 if !self.is_dragging {
1775 return None;
1776 }
1777 let (start_world, end_world) = self.drag_world_corners()?;
1778 let scroll = self.scroll_handle.offset();
1779 let sx: f32 = scroll.x.into();
1780 let sy: f32 = scroll.y.into();
1781 let start_screen = Point {
1782 x: px(f32::from(start_world.x) - sx),
1783 y: px(f32::from(start_world.y) - sy),
1784 };
1785 let end_screen = Point {
1786 x: px(f32::from(end_world.x) - sx),
1787 y: px(f32::from(end_world.y) - sy),
1788 };
1789 Some((start_screen, end_screen))
1790 }
1791
1792 fn update_drag(&mut self) {
1793 let (start_world, end_world) = match self.drag_world_corners() {
1794 Some(c) => c,
1795 None => return,
1796 };
1797 if !self.is_dragging {
1798 let dx = f32::from(end_world.x) - f32::from(start_world.x);
1799 let dy = f32::from(end_world.y) - f32::from(start_world.y);
1800 if dx * dx + dy * dy <= 400.0 {
1801 return;
1802 }
1803 self.is_dragging = true;
1804 }
1805 let r1 = match self.drag_start_hit {
1806 Some(h) => h,
1807 None => return,
1808 };
1809 let r2 = self.hit_test_content(f32::from(end_world.x), f32::from(end_world.y), 0.0, 0.0);
1813 match (r1, r2) {
1814 (HitResult::Cell(r1c, c1), HitResult::Cell(r2c, c2)) => {
1815 self.selection =
1816 Selection::CellRange(r1c.min(r2c), c1.min(c2), r1c.max(r2c), c1.max(c2));
1817 }
1818 (HitResult::RowHeader(r1r), HitResult::RowHeader(r2r)) => {
1819 self.selection = Selection::RowRange(r1r.min(r2r), r1r.max(r2r));
1820 }
1821 (
1822 HitResult::ColumnHeader(c1),
1823 HitResult::ColumnHeader(c2)
1824 | HitResult::SortButton(c2)
1825 | HitResult::ColumnBorder(c2),
1826 ) => {
1827 self.selection = if c1 == c2 {
1828 Selection::Column(c1)
1829 } else {
1830 Selection::Columns((c1.min(c2)..=c1.max(c2)).collect())
1831 };
1832 }
1833 _ => {}
1834 }
1835 }
1836
1837 fn update_drag_from_last(&mut self) {
1838 self.update_drag();
1839 }
1840
1841 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, pressed_button: Option<MouseButton>) {
1842 if self.is_dragging && pressed_button != Some(MouseButton::Left) {
1843 self.handle_mouse_up();
1844 return;
1845 }
1846 if let Some(col) = self.resizing_col {
1847 if pressed_button != Some(MouseButton::Left) {
1848 self.resizing_col = None;
1849 return;
1850 }
1851 let new_w =
1852 (self.resize_start_width + (f32::from(pos.x) - self.resize_start_x)).max(40.0);
1853 self.data.columns[col].width = new_w;
1854 return;
1855 }
1856 if let Some(axis) = self.scrollbar_drag {
1857 if pressed_button != Some(MouseButton::Left) {
1858 self.scrollbar_drag = None;
1859 return;
1860 }
1861 match axis {
1862 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
1863 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
1864 }
1865 self.last_mouse_pos = Some(pos);
1866 return;
1867 }
1868 self.last_mouse_pos = Some(pos);
1869 if self.context_menu.is_some() {
1870 return;
1875 }
1876 self.hover_hit = Some(self.hit_test(pos));
1877 if self.drag_start.is_none() {
1878 return;
1879 }
1880 self.update_drag();
1881 }
1882
1883 pub fn handle_scroll_drag(&mut self) {
1884 if self.drag_start.is_some() && self.last_mouse_pos.is_some() {
1885 self.update_drag();
1886 }
1887 }
1888
1889 pub fn handle_mouse_up(&mut self) {
1890 self.resizing_col = None;
1891 self.scrollbar_drag = None;
1892 self.clear_drag();
1893 }
1894
1895 pub fn apply_edge_scroll(&mut self) -> bool {
1896 apply_edge_scroll(self)
1897 }
1898
1899 pub fn select_all(&mut self) {
1900 let nrows = self.display_row_count();
1901 let ncols = self.data.columns.len();
1902 if nrows > 0 && ncols > 0 {
1903 self.selection = Selection::CellRange(0, 0, nrows - 1, ncols - 1);
1904 }
1905 }
1906
1907 #[must_use]
1911 pub fn selected_rows(&self) -> Vec<usize> {
1912 let nrows = self.display_row_count();
1913 match &self.selection {
1914 Selection::Row(r) if *r < nrows => vec![*r],
1915 Selection::RowRange(r1, r2) => {
1916 (*r1.min(r2)..=*r1.max(r2)).filter(|&r| r < nrows).collect()
1917 }
1918 Selection::Rows(rows) => rows.iter().copied().filter(|&r| r < nrows).collect(),
1919 _ => Vec::new(),
1920 }
1921 }
1922
1923 #[must_use]
1927 pub fn selected_columns(&self) -> Vec<usize> {
1928 let ncols = self.data.columns.len();
1929 match &self.selection {
1930 Selection::Column(c) if *c < ncols => vec![*c],
1931 Selection::Columns(cols) => cols.iter().copied().filter(|&c| c < ncols).collect(),
1932 _ => Vec::new(),
1933 }
1934 }
1935
1936 #[must_use]
1941 pub fn selected_cells(&self) -> Vec<(usize, usize)> {
1942 let nrows = self.display_row_count();
1943 let ncols = self.data.columns.len();
1944 if nrows == 0 || ncols == 0 {
1945 return Vec::new();
1946 }
1947 match &self.selection {
1948 Selection::None => Vec::new(),
1949 Selection::Cell(r, c) if *r < nrows && *c < ncols => vec![(*r, *c)],
1950 Selection::Cell(..) => Vec::new(),
1951 Selection::Cells(cells) => cells
1952 .iter()
1953 .copied()
1954 .filter(|&(r, c)| r < nrows && c < ncols)
1955 .collect(),
1956 Selection::Column(_) | Selection::Columns(_) => {
1957 let cols = self.selected_columns();
1958 (0..nrows)
1959 .flat_map(|r| cols.iter().map(move |&c| (r, c)))
1960 .collect()
1961 }
1962 Selection::Row(_) | Selection::RowRange(..) | Selection::Rows(_) => self
1963 .selected_rows()
1964 .into_iter()
1965 .flat_map(|r| (0..ncols).map(move |c| (r, c)))
1966 .collect(),
1967 Selection::CellRange(r1, c1, r2, c2) => {
1968 let (rmin, rmax) = (*r1.min(r2), *r1.max(r2));
1969 let (cmin, cmax) = (*c1.min(c2), *c1.max(c2));
1970 (rmin..=rmax.min(nrows.saturating_sub(1)))
1971 .flat_map(|r| (cmin..=cmax.min(ncols.saturating_sub(1))).map(move |c| (r, c)))
1972 .collect()
1973 }
1974 }
1975 }
1976
1977 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
1978 let Some((raw_r1, raw_c1, raw_r2, raw_c2)) = self.selection.normalized_bounds() else {
1979 return;
1980 };
1981 if self.display_row_count() == 0 || self.data.columns.is_empty() {
1982 return;
1983 }
1984 let last_row = self.display_row_count() - 1;
1985 let last_col = self.data.columns.len() - 1;
1986 let r1 = raw_r1.min(last_row);
1987 let r2 = raw_r2.min(last_row);
1988 let c1 = raw_c1.min(last_col);
1989 let c2 = raw_c2.min(last_col);
1990 let mut text = String::new();
1991 if with_headers {
1992 for c in c1..=c2 {
1993 if c > c1 {
1994 text.push('\t');
1995 }
1996 text.push_str(&self.data.columns[c].name);
1997 }
1998 text.push('\n');
1999 }
2000 for dr in r1..=r2 {
2001 let Some(row_idx) = self.resident_row_for_display(dr) else {
2002 continue;
2003 };
2004 for c in c1..=c2 {
2005 if c > c1 {
2006 text.push('\t');
2007 }
2008 let cell = &self.data.rows[row_idx][c];
2009 let (s, _) = format_cell(cell, &self.resolved_formats[c]);
2010 text.push_str(&s);
2011 }
2012 text.push('\n');
2013 }
2014 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
2015 }
2016
2017 pub fn page_up(&mut self) {
2018 let vh: f32 = self.bounds.size.height.into();
2019 let rows = ((vh - self.header_height) / self.row_height) as i32;
2020 self.move_selection(0, -rows);
2021 }
2022
2023 pub fn page_down(&mut self) {
2024 let vh: f32 = self.bounds.size.height.into();
2025 let rows = ((vh - self.header_height) / self.row_height) as i32;
2026 self.move_selection(0, rows);
2027 }
2028
2029 pub fn handle_key(&mut self, keystroke: &Keystroke) {
2030 if self.filter_panel.is_some() {
2031 match keystroke.key.as_str() {
2032 "escape" => {
2033 self.filter_panel = None;
2034 return;
2035 }
2036 "enter" => {
2037 self.apply_filter_panel();
2038 return;
2039 }
2040 _ => {}
2041 }
2042 let mut edited = false;
2043 if let Some(panel) = &mut self.filter_panel {
2044 let input = panel.active_input_mut();
2045 match keystroke.key.as_str() {
2046 "backspace" => {
2047 input.backspace();
2048 edited = true;
2049 }
2050 "left" => input.move_left(),
2051 "right" => input.move_right(),
2052 _ => {
2053 if let Some(ch) = keystroke_to_char(keystroke) {
2054 input.insert_char(ch);
2055 edited = true;
2056 }
2057 }
2058 }
2059 }
2060 if edited {
2063 self.maybe_auto_apply();
2064 }
2065 return;
2066 }
2067 if self.context_menu.is_some() {
2068 if keystroke.key.as_str() == "escape" {
2069 self.context_menu = None;
2070 }
2071 return;
2072 }
2073 let shift = keystroke.modifiers.shift;
2074 match keystroke.key.as_str() {
2075 "up" if shift => self.extend_selection(0, -1),
2076 "down" if shift => self.extend_selection(0, 1),
2077 "left" if shift => self.extend_selection(-1, 0),
2078 "right" if shift => self.extend_selection(1, 0),
2079 "up" => self.move_selection(0, -1),
2080 "down" => self.move_selection(0, 1),
2081 "left" => self.move_selection(-1, 0),
2082 "right" => self.move_selection(1, 0),
2083 "escape" => {
2084 self.selection = Selection::None;
2085 self.range_anchor = None;
2086 self.range_active = None;
2087 }
2088 _ => {}
2089 }
2090 }
2091
2092 fn move_selection(&mut self, dx: i32, dy: i32) {
2093 let nrows = self.display_row_count() as i32;
2094 let ncols = self.data.columns.len() as i32;
2095 if nrows == 0 || ncols == 0 {
2096 return;
2097 }
2098 let last_row = nrows - 1;
2099 let last_col = ncols - 1;
2100 match self.selection {
2101 Selection::Cell(row, col) => {
2102 let nr = (row as i32 + dy).clamp(0, last_row) as usize;
2103 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
2104 self.selection = Selection::Cell(nr, nc);
2105 self.range_anchor = Some((nr, nc));
2106 self.range_active = Some((nr, nc));
2107 }
2108 Selection::Row(row) if dy != 0 => {
2109 let nr = (row as i32 + dy).clamp(0, last_row) as usize;
2110 self.selection = Selection::Row(nr);
2111 }
2112 Selection::Column(col) if dx != 0 => {
2113 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
2114 self.selection = Selection::Column(nc);
2115 }
2116 _ => {
2117 self.selection = Selection::Cell(0, 0);
2118 self.range_anchor = Some((0, 0));
2119 self.range_active = Some((0, 0));
2120 }
2121 }
2122 }
2123
2124 fn extend_selection(&mut self, dx: i32, dy: i32) {
2128 let nrows = self.display_row_count() as i32;
2129 let ncols = self.data.columns.len() as i32;
2130 if nrows == 0 || ncols == 0 {
2131 return;
2132 }
2133 let last_row = nrows - 1;
2134 let last_col = ncols - 1;
2135
2136 if self.range_anchor.is_none() || self.range_active.is_none() {
2138 match self.selection {
2139 Selection::Cell(r, c) => {
2140 self.range_anchor = Some((r, c));
2141 self.range_active = Some((r, c));
2142 }
2143 Selection::CellRange(r1, c1, r2, c2) => {
2144 self.range_anchor = Some((r1, c1));
2145 self.range_active = Some((r2, c2));
2146 }
2147 _ => {
2148 self.range_anchor = Some((0, 0));
2149 self.range_active = Some((0, 0));
2150 self.selection = Selection::Cell(0, 0);
2151 }
2152 }
2153 }
2154
2155 let anchor = self.range_anchor.unwrap_or((0, 0));
2156 let active = self.range_active.unwrap_or(anchor);
2157 let nr = (active.0 as i32 + dy).clamp(0, last_row) as usize;
2158 let nc = (active.1 as i32 + dx).clamp(0, last_col) as usize;
2159 self.range_active = Some((nr, nc));
2160
2161 self.selection = if (nr, nc) == anchor {
2162 Selection::Cell(nr, nc)
2163 } else {
2164 Selection::CellRange(
2165 anchor.0.min(nr),
2166 anchor.1.min(nc),
2167 anchor.0.max(nr),
2168 anchor.1.max(nc),
2169 )
2170 };
2171 }
2172
2173 pub(crate) fn hit_test(&self, pos: Point<Pixels>) -> HitResult {
2174 let bounds = self.bounds;
2175 let (sx, sy) = (
2176 f32::from(self.scroll_handle.offset().x),
2177 f32::from(self.scroll_handle.offset().y),
2178 );
2179 let bw: f32 = bounds.size.width.into();
2180 let bh: f32 = bounds.size.height.into();
2181 let (mx, my) = self.max_scroll();
2182 if let Some(menu) = &self.context_menu {
2183 let cw = self.char_width;
2184 let x_rel = f32::from(pos.x);
2187 let y_rel = f32::from(pos.y);
2188 if let Some(idx) = menu_mod::hover_at(menu, x_rel, y_rel, cw) {
2189 return HitResult::ContextMenuItem(idx);
2190 }
2191 }
2192 if my > 0.0
2193 && f32::from(pos.x) >= bw - SCROLLBAR_SIZE
2194 && f32::from(pos.y) >= self.header_height
2195 {
2196 return HitResult::VerticalScrollbar;
2197 }
2198 if mx > 0.0
2199 && f32::from(pos.y) >= bh - SCROLLBAR_SIZE
2200 && f32::from(pos.x) >= self.row_header_width
2201 {
2202 return HitResult::HorizontalScrollbar;
2203 }
2204 let px = f32::from(pos.x);
2210 let py = f32::from(pos.y);
2211 if px < 0.0 || py < 0.0 || px > bw || py > bh {
2212 return HitResult::None;
2213 }
2214 self.hit_test_content(px, py, sx, sy)
2215 }
2216
2217 fn hit_test_content(&self, x: f32, y: f32, sx: f32, sy: f32) -> HitResult {
2218 if y < self.header_height {
2219 if x < self.row_header_width {
2220 return HitResult::Corner;
2221 }
2222 let col_x = x - self.row_header_width + sx;
2223 let mut acc = 0.0;
2224 for (i, col) in self.data.columns.iter().enumerate() {
2225 let right = acc + col.width;
2226 if i + 1 < self.data.columns.len() && col_x >= right - 5.0 && col_x <= right + 5.0 {
2227 return HitResult::ColumnBorder(i);
2228 }
2229 if col_x >= acc && col_x < right {
2230 if col_x >= right - 20.0 {
2231 return HitResult::SortButton(i);
2232 }
2233 return HitResult::ColumnHeader(i);
2234 }
2235 acc = right;
2236 }
2237 return HitResult::None;
2238 }
2239 if x < self.row_header_width {
2240 let row_y = y - self.header_height + sy;
2241 if row_y < 0.0 {
2242 return HitResult::None;
2243 }
2244 let row_idx = (row_y / self.row_height) as usize;
2245 if row_idx < self.display_row_count() {
2246 return HitResult::RowHeader(row_idx);
2247 }
2248 return HitResult::None;
2249 }
2250 let col_x = x - self.row_header_width + sx;
2251 let row_y = y - self.header_height + sy;
2252 if row_y < 0.0 {
2253 return HitResult::None;
2254 }
2255 let row_idx = (row_y / self.row_height) as usize;
2256 if row_idx >= self.display_row_count() {
2257 return HitResult::None;
2258 }
2259 let mut acc = 0.0;
2260 for (i, col) in self.data.columns.iter().enumerate() {
2261 if col_x >= acc && col_x < acc + col.width {
2262 return HitResult::Cell(row_idx, i);
2263 }
2264 acc += col.width;
2265 }
2266 HitResult::None
2267 }
2268
2269 #[must_use]
2270 pub fn wants_edge_scroll_tick(&self) -> bool {
2271 self.is_dragging
2272 }
2273}
2274
2275fn keystroke_to_char(k: &Keystroke) -> Option<char> {
2276 if k.modifiers.control || k.modifiers.platform || k.modifiers.alt {
2277 return None;
2278 }
2279 if let Some(key_char) = k.key_char.as_ref() {
2280 return key_char.chars().next();
2281 }
2282 if k.key.chars().count() == 1 {
2283 let c = k.key.chars().next()?;
2284 if k.modifiers.shift {
2285 Some(c.to_ascii_uppercase())
2286 } else {
2287 Some(c)
2288 }
2289 } else {
2290 None
2291 }
2292}
2293
2294#[cfg(test)]
2295#[allow(
2296 clippy::unwrap_used,
2297 clippy::expect_used,
2298 clippy::field_reassign_with_default
2299)]
2300mod tests {
2301 use super::*;
2302 use crate::data::{CellValue, Column, ColumnKind};
2303 use crate::grid::state::state_inner::{edge_scroll_speed, format_current_status};
2304
2305 fn input_with(text: &str, cursor: usize) -> TextInput {
2306 let mut p = TextInput::new(text.to_owned());
2307 p.cursor_chars = cursor;
2308 p
2309 }
2310
2311 #[test]
2312 fn text_input_new_cursors_at_char_count_not_bytes() {
2313 let p = TextInput::new("hé🙂".into());
2315 assert_eq!(p.cursor_chars, 3);
2316 assert_eq!(p.value.len(), 7);
2317 }
2318
2319 #[test]
2320 fn text_input_insert_emoji_at_start_does_not_panic() {
2321 let mut p = input_with("ab", 0);
2322 p.insert_char('\u{1F600}');
2323 assert_eq!(p.value, "\u{1F600}ab");
2324 assert_eq!(p.cursor_chars, 1);
2325 }
2326
2327 #[test]
2328 fn text_input_insert_in_middle_keeps_cursor_at_char_position() {
2329 let mut p = input_with("helloworld", 5);
2330 p.insert_char(' ');
2331 assert_eq!(p.value, "hello world");
2332 assert_eq!(p.cursor_chars, 6);
2333 }
2334
2335 #[test]
2336 fn text_input_backspace_at_zero_is_noop() {
2337 let mut p = input_with("abc", 0);
2338 p.backspace();
2339 assert_eq!(p.value, "abc");
2340 assert_eq!(p.cursor_chars, 0);
2341 }
2342
2343 #[test]
2344 fn text_input_backspace_removes_one_char_value() {
2345 let mut p = input_with("héx", 2);
2347 p.backspace();
2348 assert_eq!(p.value, "hx");
2349 assert_eq!(p.cursor_chars, 1);
2350 }
2351
2352 #[test]
2353 fn text_input_clamp_cursor_pulls_back_past_end() {
2354 let mut p = input_with("abc", 99);
2355 p.clamp_cursor();
2356 assert_eq!(p.cursor_chars, 3);
2357 }
2358
2359 #[test]
2360 fn text_input_move_left_and_right_respect_bounds() {
2361 let mut p = input_with("ab", 2);
2362 p.move_right();
2363 assert_eq!(p.cursor_chars, 2);
2364 p.move_left();
2365 p.move_left();
2366 p.move_left();
2367 assert_eq!(p.cursor_chars, 0);
2368 }
2369
2370 #[test]
2371 fn edge_scroll_speed_stops_outside_band() {
2372 assert_eq!(edge_scroll_speed(120.0), 0.0);
2374 assert_eq!(edge_scroll_speed(90.01), 0.0);
2375 assert_eq!(edge_scroll_speed(90.0), 4.0);
2377 assert_eq!(edge_scroll_speed(60.0), 4.0);
2378 assert_eq!(edge_scroll_speed(59.99), 8.0);
2379 assert_eq!(edge_scroll_speed(30.0), 8.0);
2381 assert_eq!(edge_scroll_speed(29.99), 16.0);
2382 assert_eq!(edge_scroll_speed(0.0), 16.0);
2384 assert_eq!(edge_scroll_speed(29.99), 16.0);
2385 }
2386
2387 #[test]
2388 fn edge_scroll_speed_caps_negative_runaway() {
2389 assert_eq!(edge_scroll_speed(-100.0), 16.0);
2391 assert_eq!(edge_scroll_speed(-1000.0), 16.0);
2392 }
2393
2394 #[allow(clippy::expect_used, clippy::unwrap_used)]
2402 #[test]
2403 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2404 fn grid_state_behavior_under_application() {
2405 gpui::Application::new().run(|cx| {
2406 let focus = cx.focus_handle();
2407
2408 let mut state = GridState::new(
2410 GridData::new(
2411 vec![Column::new("n", ColumnKind::Integer, 100.0)],
2412 vec![vec![CellValue::Integer(1)]],
2413 )
2414 .expect("rectangular"),
2415 crate::config::GridConfig::default(),
2416 focus.clone(),
2417 );
2418 let _ = format_current_status(&state);
2419 assert_eq!(state.selection, Selection::None);
2420
2421 state.last_mouse_pos = Some(Point {
2423 x: px(120.0),
2424 y: px(80.0),
2425 });
2426 let s = format_current_status(&state);
2427 assert!(s.contains("(120, 80)"), "missing positional, got: {s}");
2428
2429 let mut state = GridState::new(
2431 GridData::new(
2432 vec![Column::new("name", ColumnKind::Text, 100.0)],
2433 vec![
2434 vec![CellValue::Text("alpha".into())],
2435 vec![CellValue::Text("beeb".into())],
2436 vec![CellValue::Text("gamma".into())],
2437 ],
2438 )
2439 .expect("rectangular"),
2440 crate::config::GridConfig::default(),
2441 focus.clone(),
2442 );
2443 state.filters[0] = ColumnFilter {
2444 predicate: FilterPredicate::Text {
2445 op: TextOp::Contains,
2446 operand: "a".into(),
2447 },
2448 values: None,
2449 };
2450 state.toggle_sort(0);
2451 state.recompute();
2452 assert_eq!(state.display_indices.as_slice(), &[0, 2]);
2453 state.toggle_sort(0);
2454 state.recompute();
2455 assert_eq!(state.display_indices.as_slice(), &[2, 0]);
2456 state.filters[0] = ColumnFilter::default();
2457 state.toggle_sort(0);
2458 state.recompute();
2459 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
2460
2461 let mut state = GridState::new(
2463 GridData::new(
2464 vec![Column::new("v", ColumnKind::Integer, 80.0)],
2465 vec![vec![CellValue::Integer(1)]],
2466 )
2467 .expect("rectangular"),
2468 crate::config::GridConfig::default(),
2469 focus.clone(),
2470 );
2471 state.toggle_sort(0);
2472 assert_eq!(state.sort, Some((0, SortDirection::Ascending)));
2473 state.toggle_sort(0);
2474 assert_eq!(state.sort, Some((0, SortDirection::Descending)));
2475 state.toggle_sort(0);
2476 assert_eq!(state.sort, None);
2477
2478 let mut state = GridState::new(
2480 GridData::new(
2481 vec![
2482 Column::new("a", ColumnKind::Integer, 80.0),
2483 Column::new("b", ColumnKind::Integer, 80.0),
2484 ],
2485 vec![vec![CellValue::Integer(1), CellValue::Integer(2)]],
2486 )
2487 .expect("rectangular"),
2488 crate::config::GridConfig::default(),
2489 focus.clone(),
2490 );
2491 state.select_all();
2492 assert_eq!(state.selection, Selection::CellRange(0, 0, 0, 1));
2493
2494 let mut state = GridState::new(
2496 GridData::new(vec![Column::new("a", ColumnKind::Integer, 80.0)], vec![])
2497 .expect("rectangular"),
2498 crate::config::GridConfig::default(),
2499 focus.clone(),
2500 );
2501 state.select_all();
2502 assert_eq!(state.selection, Selection::None);
2503
2504 let mut state = GridState::new(
2506 GridData::new(
2507 vec![Column::new("v", ColumnKind::Decimal, 100.0)],
2508 vec![vec![CellValue::Decimal(1.234)]],
2509 )
2510 .expect("rectangular"),
2511 crate::config::GridConfig::default(),
2512 focus.clone(),
2513 );
2514 assert_eq!(state.resolved_formats[0].number.decimals, 2);
2515 let mut cfg = crate::config::GridConfig::default();
2516 cfg.column_overrides = vec![crate::config::ColumnOverride {
2517 number: Some(crate::config::NumberFormat {
2518 decimals: 6,
2519 ..Default::default()
2520 }),
2521 ..Default::default()
2522 }];
2523 state.set_config(cfg);
2524 assert_eq!(state.resolved_formats[0].number.decimals, 6);
2525
2526 let mut state = GridState::new(
2528 GridData::new(
2529 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2530 vec![vec![CellValue::Integer(1)]],
2531 )
2532 .expect("rectangular"),
2533 crate::config::GridConfig::default(),
2534 focus.clone(),
2535 );
2536 assert!(!state.wants_edge_scroll_tick());
2537 state.is_dragging = true;
2538 assert!(state.wants_edge_scroll_tick());
2539
2540 cx.quit();
2541 });
2542 }
2543
2544 #[allow(clippy::expect_used, clippy::unwrap_used)]
2545 #[test]
2546 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2547 fn context_menu_request_construction() {
2548 use crate::grid::context_menu::ContextMenuTarget;
2549
2550 gpui::Application::new().run(|cx| {
2551 let focus = cx.focus_handle();
2552
2553 let mut state = GridState::new(
2555 GridData::new(
2556 vec![
2557 Column::new("id", ColumnKind::Integer, 80.0),
2558 Column::new("name", ColumnKind::Text, 100.0),
2559 ],
2560 vec![
2561 vec![CellValue::Integer(1), CellValue::Text("alpha".into())],
2562 vec![CellValue::Integer(2), CellValue::Text("beta".into())],
2563 vec![CellValue::Integer(3), CellValue::Text("gamma".into())],
2564 ],
2565 )
2566 .expect("rectangular"),
2567 crate::config::GridConfig::default(),
2568 focus.clone(),
2569 );
2570 state.sort = Some((0, SortDirection::Descending));
2572 state.recompute();
2573 assert_eq!(state.display_indices.as_slice(), &[2, 1, 0]);
2574
2575 let target = ContextMenuTarget::Cell {
2577 display_row_index: 0,
2578 source_row_index: 2,
2579 column_index: 1,
2580 };
2581 let sel = Selection::Cell(0, 1);
2582 let req = state.build_context_menu_request(target, &sel);
2583 assert_eq!(req.target.column_index(), Some(1));
2584 let cells = req.selected_cells();
2585 assert_eq!(cells.len(), 1);
2586 assert_eq!(cells[0].source_row_index, 2);
2587 assert_eq!(cells[0].column_name, "name");
2588 assert_eq!(cells[0].value, CellValue::Text("gamma".into()));
2589 let rows = req.selected_rows();
2590 assert_eq!(rows.len(), 1);
2591 assert_eq!(rows[0].source_row_index, 2);
2592 assert_eq!(rows[0].value_by_name("id"), Some(&CellValue::Integer(3)));
2593
2594 let target = ContextMenuTarget::Cell {
2596 display_row_index: 0,
2597 source_row_index: 2,
2598 column_index: 0,
2599 };
2600 let sel = Selection::CellRange(0, 0, 1, 1);
2601 let req = state.build_context_menu_request(target, &sel);
2602 assert_eq!(req.selected_cell_count(), 4); let rows = req.selected_rows();
2604 assert_eq!(rows.len(), 2);
2605 assert_eq!(rows[0].source_row_index, 2);
2607 assert_eq!(rows[1].source_row_index, 1);
2608
2609 let target = ContextMenuTarget::RowHeader {
2611 display_row_index: 1,
2612 source_row_index: 1,
2613 };
2614 let sel = Selection::RowRange(0, 2);
2615 let req = state.build_context_menu_request(target, &sel);
2616 let rows = req.selected_rows();
2617 assert_eq!(rows.len(), 3);
2618 assert_eq!(rows[0].values.len(), 2);
2620 assert_eq!(req.selected_cell_count(), 6); let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
2626 let sel = Selection::Column(0);
2627 let req = state.build_context_menu_request(target, &sel);
2628 assert!(req.is_column_oriented());
2629 assert_eq!(req.selected_row_count(), 0);
2630 assert!(req.selected_rows().is_empty());
2631 assert_eq!(req.selected_cells().len(), 3); let empty_state = GridState::new(
2635 GridData::new(vec![Column::new("x", ColumnKind::Integer, 80.0)], vec![])
2636 .expect("rectangular"),
2637 crate::config::GridConfig::default(),
2638 focus.clone(),
2639 );
2640 let target = ContextMenuTarget::Cell {
2641 display_row_index: 0,
2642 source_row_index: 0,
2643 column_index: 0,
2644 };
2645 let req = empty_state.build_context_menu_request(target, &Selection::None);
2646 assert!(req.selected_cells().is_empty());
2647 assert!(req.selected_rows().is_empty());
2648
2649 cx.quit();
2650 });
2651 }
2652
2653 #[allow(clippy::expect_used, clippy::unwrap_used)]
2654 #[test]
2655 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2656 fn effective_selection_for_context_target() {
2657 gpui::Application::new().run(|cx| {
2658 let focus = cx.focus_handle();
2659 let mut state = GridState::new(
2660 GridData::new(
2661 vec![
2662 Column::new("a", ColumnKind::Integer, 80.0),
2663 Column::new("b", ColumnKind::Integer, 80.0),
2664 ],
2665 vec![
2666 vec![CellValue::Integer(1), CellValue::Integer(2)],
2667 vec![CellValue::Integer(3), CellValue::Integer(4)],
2668 ],
2669 )
2670 .expect("rectangular"),
2671 crate::config::GridConfig::default(),
2672 focus,
2673 );
2674
2675 state.selection = Selection::Cell(0, 0);
2677 let target = ContextMenuTarget::Cell {
2678 display_row_index: 1,
2679 source_row_index: 1,
2680 column_index: 1,
2681 };
2682 let eff = state.effective_selection_for_context_target(&target);
2683 assert_eq!(eff, Selection::Cell(1, 1));
2684
2685 state.selection = Selection::CellRange(0, 0, 1, 1);
2687 let target = ContextMenuTarget::Cell {
2688 display_row_index: 1,
2689 source_row_index: 1,
2690 column_index: 1,
2691 };
2692 let eff = state.effective_selection_for_context_target(&target);
2693 assert_eq!(eff, Selection::CellRange(0, 0, 1, 1));
2694
2695 state.selection = Selection::Cell(0, 0);
2697 let target = ContextMenuTarget::RowHeader {
2698 display_row_index: 1,
2699 source_row_index: 1,
2700 };
2701 let eff = state.effective_selection_for_context_target(&target);
2702 assert_eq!(eff, Selection::Row(1));
2703
2704 state.selection = Selection::RowRange(0, 1);
2706 let target = ContextMenuTarget::RowHeader {
2707 display_row_index: 1,
2708 source_row_index: 1,
2709 };
2710 let eff = state.effective_selection_for_context_target(&target);
2711 assert_eq!(eff, Selection::RowRange(0, 1));
2712
2713 state.selection = Selection::Cell(1, 1);
2715 let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
2716 let eff = state.effective_selection_for_context_target(&target);
2717 assert_eq!(eff, Selection::Cell(1, 1));
2718
2719 cx.quit();
2720 });
2721 }
2722
2723 #[allow(clippy::expect_used, clippy::unwrap_used)]
2724 #[test]
2725 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2726 fn context_menu_target_from_hit_maps_correctly() {
2727 gpui::Application::new().run(|cx| {
2728 let focus = cx.focus_handle();
2729 let state = GridState::new(
2730 GridData::new(
2731 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2732 vec![vec![CellValue::Integer(1)], vec![CellValue::Integer(2)]],
2733 )
2734 .expect("rectangular"),
2735 crate::config::GridConfig::default(),
2736 focus,
2737 );
2738
2739 let t = state
2741 .context_menu_target_from_hit(HitResult::Cell(1, 0))
2742 .unwrap();
2743 assert_eq!(
2744 t,
2745 ContextMenuTarget::Cell {
2746 display_row_index: 1,
2747 source_row_index: 1,
2748 column_index: 0,
2749 }
2750 );
2751
2752 let t = state
2754 .context_menu_target_from_hit(HitResult::RowHeader(0))
2755 .unwrap();
2756 assert_eq!(
2757 t,
2758 ContextMenuTarget::RowHeader {
2759 display_row_index: 0,
2760 source_row_index: 0,
2761 }
2762 );
2763
2764 let t = state
2766 .context_menu_target_from_hit(HitResult::ColumnHeader(0))
2767 .unwrap();
2768 assert_eq!(t, ContextMenuTarget::ColumnHeader { column_index: 0 });
2769
2770 let t = state
2772 .context_menu_target_from_hit(HitResult::SortButton(0))
2773 .unwrap();
2774 assert_eq!(t, ContextMenuTarget::SortButton { column_index: 0 });
2775
2776 assert!(state
2778 .context_menu_target_from_hit(HitResult::VerticalScrollbar)
2779 .is_none());
2780 assert!(state
2781 .context_menu_target_from_hit(HitResult::None)
2782 .is_none());
2783
2784 cx.quit();
2785 });
2786 }
2787
2788 #[allow(clippy::expect_used, clippy::unwrap_used)]
2789 #[test]
2790 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2791 fn convert_context_menu_items_maps_variants() {
2792 use crate::grid::context_menu::ContextMenuItem;
2793
2794 let items = vec![
2795 ContextMenuItem::BuiltIn(MenuAction::SortAscending),
2796 ContextMenuItem::action("copy", "Copy value"),
2797 ContextMenuItem::separator(),
2798 ];
2799 let internal = GridState::convert_context_menu_items(items);
2800 assert!(matches!(
2801 internal[0],
2802 MenuItem::Action(MenuAction::SortAscending)
2803 ));
2804 assert!(
2805 matches!(&internal[1], MenuItem::Custom { id, label } if id == "copy" && label == "Copy value")
2806 );
2807 assert!(matches!(internal[2], MenuItem::Separator));
2808 }
2809
2810 #[allow(clippy::expect_used, clippy::unwrap_used)]
2811 #[test]
2812 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2813 fn execute_custom_context_menu_action_invokes_provider() {
2814 use crate::grid::context_menu::{
2815 ContextMenuProvider, ContextMenuProviderHandle, ContextMenuRequest,
2816 };
2817 use std::sync::{Arc, Mutex};
2818
2819 #[derive(Default)]
2820 struct TestProvider {
2821 last_action: Arc<Mutex<Option<String>>>,
2822 }
2823 impl ContextMenuProvider for TestProvider {
2824 fn menu_items(&self, _request: &ContextMenuRequest) -> Vec<ContextMenuItem> {
2825 vec![ContextMenuItem::action("test", "Test")]
2826 }
2827 fn on_action(
2828 &self,
2829 action_id: &str,
2830 _request: &ContextMenuRequest,
2831 _state: &mut GridState,
2832 _cx: &mut gpui::App,
2833 ) {
2834 *self.last_action.lock().unwrap() = Some(action_id.to_string());
2835 }
2836 }
2837
2838 gpui::Application::new().run(|cx| {
2839 let focus = cx.focus_handle();
2840 let mut state = GridState::new(
2841 GridData::new(
2842 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2843 vec![vec![CellValue::Integer(1)]],
2844 )
2845 .expect("rectangular"),
2846 crate::config::GridConfig::default(),
2847 focus,
2848 );
2849
2850 let last = Arc::new(Mutex::new(None));
2851 state.context_menu_provider = Some(ContextMenuProviderHandle::new(TestProvider {
2852 last_action: last.clone(),
2853 }));
2854
2855 let target = ContextMenuTarget::Cell {
2856 display_row_index: 0,
2857 source_row_index: 0,
2858 column_index: 0,
2859 };
2860 let request = state.build_context_menu_request(target, &Selection::Cell(0, 0));
2861 state.execute_custom_context_menu_action(
2862 PendingCustomContextMenuAction {
2863 id: "test".into(),
2864 request,
2865 },
2866 cx,
2867 );
2868 assert_eq!(*last.lock().unwrap(), Some("test".to_string()));
2869 assert!(state.context_menu.is_none());
2870
2871 cx.quit();
2872 });
2873 }
2874
2875 #[test]
2876 fn filter_panel_to_filter_with_all_checked_has_no_value_set() {
2877 let panel = FilterPanel {
2878 col: 0,
2879 anchor: Point {
2880 x: px(0.0),
2881 y: px(0.0),
2882 },
2883 kind: ColumnKind::Text,
2884 search: TextInput::default(),
2885 op_index: 0,
2886 op_menu_open: false,
2887 operand_a: TextInput::default(),
2888 operand_b: TextInput::default(),
2889 focus: FilterInput::Search,
2890 auto_apply: true,
2891 distinct: vec![
2892 FilterValueRow {
2893 label: "alpha".into(),
2894 checked: true,
2895 },
2896 FilterValueRow {
2897 label: "beta".into(),
2898 checked: true,
2899 },
2900 ],
2901 };
2902 let f = panel.to_filter();
2903 assert!(f.values.is_none(), "all checked => no value allow-list");
2904 assert!(
2905 !f.is_active(),
2906 "default predicate + all checked => inactive"
2907 );
2908 }
2909
2910 #[test]
2911 fn filter_panel_to_filter_with_unchecked_value_builds_allow_set() {
2912 let panel = FilterPanel {
2913 col: 0,
2914 anchor: Point {
2915 x: px(0.0),
2916 y: px(0.0),
2917 },
2918 kind: ColumnKind::Text,
2919 search: TextInput::default(),
2920 op_index: 0,
2921 op_menu_open: false,
2922 operand_a: TextInput::default(),
2923 operand_b: TextInput::default(),
2924 focus: FilterInput::Search,
2925 auto_apply: true,
2926 distinct: vec![
2927 FilterValueRow {
2928 label: "alpha".into(),
2929 checked: true,
2930 },
2931 FilterValueRow {
2932 label: "beta".into(),
2933 checked: false,
2934 },
2935 ],
2936 };
2937 let f = panel.to_filter();
2938 assert!(f.is_active(), "unchecked value => active filter");
2939 let set = f.values.expect("should have a value set");
2940 assert!(set.contains("alpha"));
2941 assert!(!set.contains("beta"));
2942 }
2943
2944 #[test]
2945 fn filter_panel_visible_indices_respects_search() {
2946 let panel = FilterPanel {
2947 col: 0,
2948 anchor: Point {
2949 x: px(0.0),
2950 y: px(0.0),
2951 },
2952 kind: ColumnKind::Text,
2953 search: TextInput::new("al".into()),
2954 op_index: 0,
2955 op_menu_open: false,
2956 operand_a: TextInput::default(),
2957 operand_b: TextInput::default(),
2958 focus: FilterInput::Search,
2959 auto_apply: true,
2960 distinct: vec![
2961 FilterValueRow {
2962 label: "alpha".into(),
2963 checked: true,
2964 },
2965 FilterValueRow {
2966 label: "beta".into(),
2967 checked: true,
2968 },
2969 FilterValueRow {
2970 label: "gamma".into(),
2971 checked: true,
2972 },
2973 ],
2974 };
2975 let vis = panel.visible_indices();
2976 assert_eq!(vis, vec![0], "search 'al' matches only alpha");
2977 }
2978
2979 #[test]
2980 fn filter_panel_all_checked_ignores_search() {
2981 let mut panel = FilterPanel {
2982 col: 0,
2983 anchor: Point {
2984 x: px(0.0),
2985 y: px(0.0),
2986 },
2987 kind: ColumnKind::Text,
2988 search: TextInput::new("al".into()),
2989 op_index: 0,
2990 op_menu_open: false,
2991 operand_a: TextInput::default(),
2992 operand_b: TextInput::default(),
2993 focus: FilterInput::Search,
2994 auto_apply: true,
2995 distinct: vec![
2996 FilterValueRow {
2997 label: "alpha".into(),
2998 checked: true,
2999 },
3000 FilterValueRow {
3001 label: "beta".into(),
3002 checked: false,
3003 },
3004 FilterValueRow {
3005 label: "gamma".into(),
3006 checked: true,
3007 },
3008 ],
3009 };
3010 assert!(
3013 !panel.all_checked(),
3014 "beta is unchecked, so not all values are checked (search is irrelevant)"
3015 );
3016
3017 panel.search = TextInput::new("zzz".into());
3019 for row in &mut panel.distinct {
3020 row.checked = true;
3021 }
3022 assert!(
3023 panel.all_checked(),
3024 "all values checked -> Select All stays checked regardless of empty search"
3025 );
3026 }
3027
3028 #[allow(clippy::expect_used, clippy::unwrap_used)]
3029 #[test]
3030 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
3031 fn filter_panel_open_apply_clear_state_flow() {
3032 gpui::Application::new().run(|cx| {
3033 let focus = cx.focus_handle();
3034 let mut state = GridState::new(
3035 GridData::new(
3036 vec![Column::new("name", ColumnKind::Text, 100.0)],
3037 vec![
3038 vec![CellValue::Text("alpha".into())],
3039 vec![CellValue::Text("beta".into())],
3040 vec![CellValue::Text("gamma".into())],
3041 ],
3042 )
3043 .expect("rectangular"),
3044 crate::config::GridConfig::default(),
3045 focus,
3046 );
3047
3048 let anchor = Point {
3050 x: px(50.0),
3051 y: px(20.0),
3052 };
3053 state.open_filter_panel(0, Some(anchor));
3054 let panel = state.filter_panel.as_ref().expect("panel should be open");
3055 assert_eq!(panel.col, 0);
3056 assert_eq!(panel.anchor, anchor);
3057 assert_eq!(panel.distinct.len(), 3);
3058 assert!(
3059 panel.distinct.iter().all(|r| r.checked),
3060 "all checked by default"
3061 );
3062 assert!(panel.auto_apply, "auto_apply defaults to true");
3063 assert_eq!(panel.kind, ColumnKind::Text);
3064
3065 state.toggle_filter_value(1);
3067 state.apply_filter_panel();
3068 assert_eq!(
3069 state.display_indices.as_slice(),
3070 &[0, 2],
3071 "beta should be filtered out"
3072 );
3073
3074 state.clear_filter_panel();
3076 assert_eq!(
3077 state.display_indices.as_slice(),
3078 &[0, 1, 2],
3079 "all rows visible after clear"
3080 );
3081 assert!(
3082 state.filters[0] == ColumnFilter::default(),
3083 "filter reset to default"
3084 );
3085
3086 state.open_filter_panel(0, Some(anchor));
3088 let panel = state.filter_panel.as_mut().expect("panel open");
3089 panel.op_index = 1; panel.operand_a = TextInput::new("a".into());
3091 state.apply_filter_panel();
3092 assert_eq!(
3093 state.display_indices.as_slice(),
3094 &[0, 2],
3095 "contains 'a' matches alpha and gamma"
3096 );
3097
3098 state.clear_filter_panel();
3100 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
3101
3102 cx.quit();
3103 });
3104 }
3105}