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 let hit = self.hit_test(pos);
1098 self.click_pos = Some(pos);
1099 self.click_hit = Some(hit);
1100 match hit {
1101 HitResult::VerticalScrollbar => {
1102 self.scrollbar_drag = Some(ScrollbarAxis::Vertical);
1103 self.scroll_to_vbar(f32::from(pos.y));
1104 self.clear_drag();
1105 }
1106 HitResult::HorizontalScrollbar => {
1107 self.scrollbar_drag = Some(ScrollbarAxis::Horizontal);
1108 self.scroll_to_hbar(f32::from(pos.x));
1109 self.clear_drag();
1110 }
1111 HitResult::ColumnBorder(col) => {
1112 self.resizing_col = Some(col);
1113 self.resize_start_x = f32::from(pos.x);
1114 self.resize_start_width = self.data.columns[col].width;
1115 self.clear_drag();
1116 }
1117 HitResult::ColumnHeader(col) => {
1118 self.selection = Selection::Column(col);
1119 self.clear_drag();
1120 }
1121 HitResult::SortButton(col) => {
1122 self.toggle_sort(col);
1125 self.clear_drag();
1126 }
1127 HitResult::ContextMenuItem(_) => {}
1128 HitResult::RowHeader(row) => {
1129 self.selection = if shift {
1130 if let Selection::Row(prev) = self.selection {
1131 let (s, e) = (prev, row);
1132 Selection::RowRange(s.min(e), s.max(e))
1133 } else {
1134 Selection::Row(row)
1135 }
1136 } else {
1137 Selection::Row(row)
1138 };
1139 self.start_drag(pos);
1140 self.drag_start_hit = Some(HitResult::RowHeader(row));
1141 }
1142 HitResult::Cell(row, col) => {
1143 self.selection = if shift {
1144 let anchor = self
1146 .range_anchor
1147 .or(match self.selection {
1148 Selection::Cell(pr, pc) => Some((pr, pc)),
1149 _ => None,
1150 })
1151 .unwrap_or((row, col));
1152 self.range_anchor = Some(anchor);
1153 self.range_active = Some((row, col));
1154 Selection::CellRange(
1155 anchor.0.min(row),
1156 anchor.1.min(col),
1157 anchor.0.max(row),
1158 anchor.1.max(col),
1159 )
1160 } else {
1161 self.range_anchor = Some((row, col));
1162 self.range_active = Some((row, col));
1163 Selection::Cell(row, col)
1164 };
1165 self.start_drag(pos);
1166 self.drag_start_hit = Some(HitResult::Cell(row, col));
1167 }
1168 HitResult::Corner | HitResult::None => {
1169 self.selection = Selection::None;
1170 self.range_anchor = None;
1171 self.range_active = None;
1172 self.context_menu = None;
1173 self.filter_panel = None;
1174 self.clear_drag();
1175 }
1176 }
1177 }
1178
1179 fn start_drag(&mut self, pos: Point<Pixels>) {
1180 self.is_dragging = false;
1181 self.drag_start = Some(pos);
1182 self.scroll_at_click = Some(self.scroll_handle.offset());
1183 self.last_mouse_pos = Some(pos);
1184 }
1185
1186 pub(crate) fn open_context_menu(&mut self, col: usize, anchor: Point<Pixels>) {
1187 self.context_menu = Some(menu_mod::ContextMenu::standard(col, anchor));
1188 self.filter_panel = None;
1189 }
1190
1191 pub(crate) fn context_menu_target_from_hit(&self, hit: HitResult) -> Option<ContextMenuTarget> {
1194 match hit {
1195 HitResult::Cell(row, col) => {
1196 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1197 Some(ContextMenuTarget::Cell {
1198 display_row_index: row,
1199 source_row_index: source_row,
1200 column_index: col,
1201 })
1202 }
1203 HitResult::RowHeader(row) => {
1204 let source_row = self.resident_row_for_display(row).unwrap_or(row);
1205 Some(ContextMenuTarget::RowHeader {
1206 display_row_index: row,
1207 source_row_index: source_row,
1208 })
1209 }
1210 HitResult::ColumnHeader(col) => {
1211 Some(ContextMenuTarget::ColumnHeader { column_index: col })
1212 }
1213 HitResult::SortButton(col) => Some(ContextMenuTarget::SortButton { column_index: col }),
1214 _ => None,
1215 }
1216 }
1217
1218 pub(crate) fn effective_selection_for_context_target(
1223 &self,
1224 target: &ContextMenuTarget,
1225 ) -> Selection {
1226 match target {
1227 ContextMenuTarget::Cell {
1228 display_row_index,
1229 column_index,
1230 ..
1231 } => {
1232 if is_cell_selected(&self.selection, *display_row_index, *column_index) {
1233 self.selection.clone()
1234 } else {
1235 Selection::Cell(*display_row_index, *column_index)
1236 }
1237 }
1238 ContextMenuTarget::RowHeader {
1239 display_row_index, ..
1240 } => {
1241 if is_row_selected(&self.selection, *display_row_index) {
1242 self.selection.clone()
1243 } else {
1244 Selection::Row(*display_row_index)
1245 }
1246 }
1247 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. } => {
1248 self.selection.clone()
1249 }
1250 }
1251 }
1252
1253 pub(crate) fn build_context_menu_request(
1265 &self,
1266 target: ContextMenuTarget,
1267 selection: &Selection,
1268 ) -> ContextMenuRequest {
1269 let mut target = target;
1276 let mut selection = selection.clone();
1277 if let Some(w) = self.window {
1278 let resident_last = self.data.rows.len().saturating_sub(1);
1279 let to_resident = |dr: usize| dr.saturating_sub(w.offset).min(resident_last);
1280 target = match target {
1281 ContextMenuTarget::Cell {
1282 display_row_index,
1283 source_row_index,
1284 column_index,
1285 } => ContextMenuTarget::Cell {
1286 display_row_index: to_resident(display_row_index),
1287 source_row_index,
1288 column_index,
1289 },
1290 ContextMenuTarget::RowHeader {
1291 display_row_index,
1292 source_row_index,
1293 } => ContextMenuTarget::RowHeader {
1294 display_row_index: to_resident(display_row_index),
1295 source_row_index,
1296 },
1297 other => other,
1298 };
1299 selection = match selection {
1300 Selection::Cell(r, c) => Selection::Cell(to_resident(r), c),
1301 Selection::Row(r) => Selection::Row(to_resident(r)),
1302 Selection::CellRange(r1, c1, r2, c2) => {
1303 Selection::CellRange(to_resident(r1), c1, to_resident(r2), c2)
1304 }
1305 other => other,
1306 };
1307 }
1308 let selection = &selection;
1309
1310 let nrows = self.display_indices.len();
1311 let ncols = self.data.columns.len();
1312
1313 let (r1, c1, r2, c2) = match selection.normalized_bounds() {
1314 Some((r1, c1, r2, c2)) => {
1315 let r1 = r1.min(nrows.saturating_sub(1));
1316 let r2 = r2.min(nrows.saturating_sub(1));
1317 let c1 = c1.min(ncols.saturating_sub(1));
1318 let c2 = c2.min(ncols.saturating_sub(1));
1319 (r1, c1, r2, c2)
1320 }
1321 None => match &target {
1322 ContextMenuTarget::Cell {
1323 display_row_index,
1324 column_index,
1325 ..
1326 } => (
1327 *display_row_index,
1328 *column_index,
1329 *display_row_index,
1330 *column_index,
1331 ),
1332 ContextMenuTarget::RowHeader {
1333 display_row_index, ..
1334 } => (
1335 *display_row_index,
1336 0,
1337 *display_row_index,
1338 ncols.saturating_sub(1),
1339 ),
1340 ContextMenuTarget::ColumnHeader { column_index }
1341 | ContextMenuTarget::SortButton { column_index } => {
1342 (0, *column_index, nrows.saturating_sub(1), *column_index)
1343 }
1344 },
1345 };
1346
1347 let menu_selection = ContextMenuSelection {
1348 row_start: r1,
1349 row_end: r2,
1350 column_start: c1,
1351 column_end: c2,
1352 };
1353
1354 let column_oriented = matches!(
1359 target,
1360 ContextMenuTarget::ColumnHeader { .. } | ContextMenuTarget::SortButton { .. }
1361 ) || matches!(selection, Selection::Column(_));
1362
1363 ContextMenuRequest::new(
1364 target,
1365 Some(menu_selection),
1366 Arc::clone(&self.data_rows),
1367 Arc::clone(&self.display_indices),
1368 Arc::clone(&self.column_meta),
1369 column_oriented,
1370 )
1371 }
1372
1373 pub(crate) fn execute_custom_context_menu_action(
1377 &mut self,
1378 pending: PendingCustomContextMenuAction,
1379 cx: &mut App,
1380 ) {
1381 self.context_menu = None;
1382 self.filter_panel = None;
1383
1384 let Some(provider) = self.context_menu_provider.clone() else {
1385 return;
1386 };
1387
1388 provider.on_action(&pending.id, &pending.request, self, cx);
1389 }
1390
1391 pub(crate) fn convert_context_menu_items(items: Vec<ContextMenuItem>) -> Vec<MenuItem> {
1394 items
1395 .into_iter()
1396 .map(|item| match item {
1397 ContextMenuItem::BuiltIn(action) => MenuItem::Action(action),
1398 ContextMenuItem::Action { id, label } => MenuItem::Custom { id, label },
1399 ContextMenuItem::Separator => MenuItem::Separator,
1400 })
1401 .collect()
1402 }
1403
1404 pub fn execute_action(&mut self, action: MenuAction, col: usize, cx: &mut App) {
1405 match action {
1406 MenuAction::SelectColumn => {
1407 self.selection = Selection::Column(col);
1408 }
1409 MenuAction::CopyColumn => {
1410 let text = self.column_text(col);
1411 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1412 }
1413 MenuAction::CopyColumnWithHeaders => {
1414 let mut text = String::new();
1415 text.push_str(&self.data.columns[col].name);
1416 text.push('\n');
1417 text.push_str(&self.column_text(col));
1418 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1419 }
1420 MenuAction::SortAscending => {
1421 self.sort = Some((col, SortDirection::Ascending));
1422 self.recompute();
1423 }
1424 MenuAction::SortDescending => {
1425 self.sort = Some((col, SortDirection::Descending));
1426 self.recompute();
1427 }
1428 MenuAction::ClearSort => {
1429 self.sort = None;
1430 self.recompute();
1431 }
1432 MenuAction::FilterPrompt => {
1433 let anchor = self.context_menu.as_ref().map(|m| m.anchor);
1434 self.open_filter_panel(col, anchor);
1435 }
1436 MenuAction::ClearFilter => {
1437 if col < self.filters.len() {
1438 self.filters[col] = ColumnFilter::default();
1439 self.recompute();
1440 }
1441 }
1442 }
1443 self.context_menu = None;
1444 }
1445
1446 pub fn open_filter_panel(&mut self, col: usize, _anchor: Option<Point<Pixels>>) {
1457 if col >= self.data.columns.len() {
1458 return;
1459 }
1460 let sx = f32::from(self.scroll_handle.offset().x);
1461 let col_x = self.row_header_width
1462 + self.data.columns[..col]
1463 .iter()
1464 .map(|c| c.width)
1465 .sum::<f32>()
1466 - sx;
1467 let anchor = Point {
1468 x: px(col_x + self.data.columns[col].width * 0.5),
1469 y: px(0.0),
1470 };
1471 let kind = self.data.columns[col].kind;
1472 let existing = self.filters.get(col).cloned().unwrap_or_default();
1473
1474 let distinct = {
1476 let fmt = &self.resolved_formats[col];
1477 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1478 let mut pairs: Vec<(String, &CellValue)> = Vec::new();
1479 for row in &self.data.rows {
1480 let cell = &row[col];
1481 let (label, _) = format_cell(cell, fmt);
1482 if seen.insert(label.clone()) {
1483 pairs.push((label, cell));
1484 }
1485 }
1486 pairs.sort_by(|(_, a), (_, b)| compare_cells(a, b));
1487 pairs
1488 .into_iter()
1489 .map(|(label, _)| {
1490 let checked = match &existing.values {
1491 None => true,
1492 Some(set) => set.contains(&label),
1493 };
1494 FilterValueRow { label, checked }
1495 })
1496 .collect()
1497 };
1498
1499 let (op_index, operand_a, operand_b) = seed_operator(kind, &existing.predicate);
1500
1501 self.context_menu = None;
1502 self.filter_panel = Some(FilterPanel {
1503 col,
1504 anchor,
1505 kind,
1506 search: TextInput::default(),
1507 op_index,
1508 op_menu_open: false,
1509 operand_a: TextInput::new(operand_a),
1510 operand_b: TextInput::new(operand_b),
1511 focus: FilterInput::Search,
1512 auto_apply: true,
1513 distinct,
1514 });
1515 }
1516
1517 pub fn apply_filter_panel(&mut self) {
1520 let Some(panel) = &self.filter_panel else {
1521 return;
1522 };
1523 let col = panel.col;
1524 let filter = panel.to_filter();
1525 if col < self.filters.len() {
1526 self.filters[col] = filter;
1527 self.recompute();
1528 }
1529 }
1530
1531 pub fn maybe_auto_apply(&mut self) {
1533 if self.filter_panel.is_some() {
1534 self.apply_filter_panel();
1535 }
1536 }
1537
1538 pub fn clear_filter_panel(&mut self) {
1541 let mut target_col = None;
1542 if let Some(panel) = &mut self.filter_panel {
1543 panel.op_index = 0;
1544 panel.op_menu_open = false;
1545 panel.operand_a = TextInput::default();
1546 panel.operand_b = TextInput::default();
1547 panel.search = TextInput::default();
1548 for row in &mut panel.distinct {
1549 row.checked = true;
1550 }
1551 target_col = Some(panel.col);
1552 }
1553 if let Some(col) = target_col {
1554 if col < self.filters.len() {
1555 self.filters[col] = ColumnFilter::default();
1556 }
1557 }
1558 self.recompute();
1559 }
1560
1561 pub fn set_panel_sort(&mut self, direction: SortDirection) {
1564 if let Some(panel) = &self.filter_panel {
1565 let col = panel.col;
1566 self.sort = match self.sort {
1567 Some((c, d)) if c == col && d == direction => None,
1568 _ => Some((col, direction)),
1569 };
1570 self.recompute();
1571 }
1572 }
1573
1574 pub fn toggle_filter_value(&mut self, index: usize) {
1577 if let Some(panel) = &mut self.filter_panel {
1578 if let Some(row) = panel.distinct.get_mut(index) {
1579 row.checked = !row.checked;
1580 }
1581 }
1582 self.maybe_auto_apply();
1583 }
1584
1585 pub fn toggle_filter_select_all(&mut self) {
1590 if let Some(panel) = &mut self.filter_panel {
1591 let target = !panel.all_checked();
1592 for row in &mut panel.distinct {
1593 row.checked = target;
1594 }
1595 }
1596 self.maybe_auto_apply();
1597 }
1598
1599 pub fn set_filter_operator(&mut self, op_index: usize) {
1602 if let Some(panel) = &mut self.filter_panel {
1603 panel.op_index = op_index;
1604 panel.op_menu_open = false;
1605 if op_index != 0 {
1606 panel.focus = FilterInput::OperandA;
1607 }
1608 }
1609 self.maybe_auto_apply();
1610 }
1611
1612 pub fn toggle_filter_op_menu(&mut self) {
1614 if let Some(panel) = &mut self.filter_panel {
1615 panel.op_menu_open = !panel.op_menu_open;
1616 }
1617 }
1618
1619 pub fn set_filter_focus(&mut self, focus: FilterInput) {
1621 if let Some(panel) = &mut self.filter_panel {
1622 panel.focus = focus;
1623 }
1624 }
1625
1626 pub fn toggle_filter_auto_apply(&mut self) {
1628 if let Some(panel) = &mut self.filter_panel {
1629 panel.auto_apply = !panel.auto_apply;
1630 }
1631 self.maybe_auto_apply();
1632 }
1633
1634 fn column_text(&self, col: usize) -> String {
1635 let mut text = String::new();
1636 let fmt = &self.resolved_formats[col];
1637 for &row_idx in self.display_indices.iter() {
1638 let cell = &self.data.rows[row_idx][col];
1639 let (s, _) = format_cell(cell, fmt);
1640 text.push_str(&s);
1641 text.push('\n');
1642 }
1643 text
1644 }
1645
1646 fn clear_drag(&mut self) {
1647 self.is_dragging = false;
1648 self.drag_start = None;
1649 self.drag_start_hit = None;
1650 self.scroll_at_click = None;
1651 }
1652
1653 fn drag_world_corners(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
1654 let start = self.drag_start?;
1655 let mouse = self.last_mouse_pos?;
1656 let click_scroll = self
1657 .scroll_at_click
1658 .unwrap_or_else(|| self.scroll_handle.offset());
1659 let scroll = self.scroll_handle.offset();
1660 let sx_click: f32 = click_scroll.x.into();
1661 let sy_click: f32 = click_scroll.y.into();
1662 let sx: f32 = scroll.x.into();
1663 let sy: f32 = scroll.y.into();
1664 let sx0: f32 = start.x.into();
1665 let sy0: f32 = start.y.into();
1666 let mx: f32 = mouse.x.into();
1667 let my: f32 = mouse.y.into();
1668 let start_world = Point {
1669 x: px(sx0 + sx_click),
1670 y: px(sy0 + sy_click),
1671 };
1672 let end_world = Point {
1673 x: px(mx + sx),
1674 y: px(my + sy),
1675 };
1676 Some((start_world, end_world))
1677 }
1678
1679 pub fn drag_screen_rect(&self) -> Option<(Point<Pixels>, Point<Pixels>)> {
1680 if !self.is_dragging {
1681 return None;
1682 }
1683 let (start_world, end_world) = self.drag_world_corners()?;
1684 let scroll = self.scroll_handle.offset();
1685 let sx: f32 = scroll.x.into();
1686 let sy: f32 = scroll.y.into();
1687 let start_screen = Point {
1688 x: px(f32::from(start_world.x) - sx),
1689 y: px(f32::from(start_world.y) - sy),
1690 };
1691 let end_screen = Point {
1692 x: px(f32::from(end_world.x) - sx),
1693 y: px(f32::from(end_world.y) - sy),
1694 };
1695 Some((start_screen, end_screen))
1696 }
1697
1698 fn update_drag(&mut self) {
1699 let (start_world, end_world) = match self.drag_world_corners() {
1700 Some(c) => c,
1701 None => return,
1702 };
1703 if !self.is_dragging {
1704 let dx = f32::from(end_world.x) - f32::from(start_world.x);
1705 let dy = f32::from(end_world.y) - f32::from(start_world.y);
1706 if dx * dx + dy * dy <= 400.0 {
1707 return;
1708 }
1709 self.is_dragging = true;
1710 }
1711 let r1 = match self.drag_start_hit {
1712 Some(h) => h,
1713 None => return,
1714 };
1715 let r2 = self.hit_test_content(f32::from(end_world.x), f32::from(end_world.y), 0.0, 0.0);
1719 match (r1, r2) {
1720 (HitResult::Cell(r1c, c1), HitResult::Cell(r2c, c2)) => {
1721 self.selection =
1722 Selection::CellRange(r1c.min(r2c), c1.min(c2), r1c.max(r2c), c1.max(c2));
1723 }
1724 (HitResult::RowHeader(r1r), HitResult::RowHeader(r2r)) => {
1725 self.selection = Selection::RowRange(r1r.min(r2r), r1r.max(r2r));
1726 }
1727 _ => {}
1728 }
1729 }
1730
1731 fn update_drag_from_last(&mut self) {
1732 self.update_drag();
1733 }
1734
1735 pub fn handle_mouse_move(&mut self, pos: Point<Pixels>, pressed_button: Option<MouseButton>) {
1736 if self.is_dragging && pressed_button != Some(MouseButton::Left) {
1737 self.handle_mouse_up();
1738 return;
1739 }
1740 if let Some(col) = self.resizing_col {
1741 if pressed_button != Some(MouseButton::Left) {
1742 self.resizing_col = None;
1743 return;
1744 }
1745 let new_w =
1746 (self.resize_start_width + (f32::from(pos.x) - self.resize_start_x)).max(40.0);
1747 self.data.columns[col].width = new_w;
1748 return;
1749 }
1750 if let Some(axis) = self.scrollbar_drag {
1751 if pressed_button != Some(MouseButton::Left) {
1752 self.scrollbar_drag = None;
1753 return;
1754 }
1755 match axis {
1756 ScrollbarAxis::Vertical => self.scroll_to_vbar(f32::from(pos.y)),
1757 ScrollbarAxis::Horizontal => self.scroll_to_hbar(f32::from(pos.x)),
1758 }
1759 self.last_mouse_pos = Some(pos);
1760 return;
1761 }
1762 self.last_mouse_pos = Some(pos);
1763 if self.context_menu.is_some() {
1764 return;
1769 }
1770 self.hover_hit = Some(self.hit_test(pos));
1771 if self.drag_start.is_none() {
1772 return;
1773 }
1774 self.update_drag();
1775 }
1776
1777 pub fn handle_scroll_drag(&mut self) {
1778 if self.drag_start.is_some() && self.last_mouse_pos.is_some() {
1779 self.update_drag();
1780 }
1781 }
1782
1783 pub fn handle_mouse_up(&mut self) {
1784 self.resizing_col = None;
1785 self.scrollbar_drag = None;
1786 self.clear_drag();
1787 }
1788
1789 pub fn apply_edge_scroll(&mut self) -> bool {
1790 apply_edge_scroll(self)
1791 }
1792
1793 pub fn select_all(&mut self) {
1794 let nrows = self.display_row_count();
1795 let ncols = self.data.columns.len();
1796 if nrows > 0 && ncols > 0 {
1797 self.selection = Selection::CellRange(0, 0, nrows - 1, ncols - 1);
1798 }
1799 }
1800
1801 pub fn copy_selection(&self, with_headers: bool, cx: &mut App) {
1802 let Some((raw_r1, raw_c1, raw_r2, raw_c2)) = self.selection.normalized_bounds() else {
1803 return;
1804 };
1805 if self.display_row_count() == 0 || self.data.columns.is_empty() {
1806 return;
1807 }
1808 let last_row = self.display_row_count() - 1;
1809 let last_col = self.data.columns.len() - 1;
1810 let r1 = raw_r1.min(last_row);
1811 let r2 = raw_r2.min(last_row);
1812 let c1 = raw_c1.min(last_col);
1813 let c2 = raw_c2.min(last_col);
1814 let mut text = String::new();
1815 if with_headers {
1816 for c in c1..=c2 {
1817 if c > c1 {
1818 text.push('\t');
1819 }
1820 text.push_str(&self.data.columns[c].name);
1821 }
1822 text.push('\n');
1823 }
1824 for dr in r1..=r2 {
1825 let Some(row_idx) = self.resident_row_for_display(dr) else {
1826 continue;
1827 };
1828 for c in c1..=c2 {
1829 if c > c1 {
1830 text.push('\t');
1831 }
1832 let cell = &self.data.rows[row_idx][c];
1833 let (s, _) = format_cell(cell, &self.resolved_formats[c]);
1834 text.push_str(&s);
1835 }
1836 text.push('\n');
1837 }
1838 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
1839 }
1840
1841 pub fn page_up(&mut self) {
1842 let vh: f32 = self.bounds.size.height.into();
1843 let rows = ((vh - self.header_height) / self.row_height) as i32;
1844 self.move_selection(0, -rows);
1845 }
1846
1847 pub fn page_down(&mut self) {
1848 let vh: f32 = self.bounds.size.height.into();
1849 let rows = ((vh - self.header_height) / self.row_height) as i32;
1850 self.move_selection(0, rows);
1851 }
1852
1853 pub fn handle_key(&mut self, keystroke: &Keystroke) {
1854 if self.filter_panel.is_some() {
1855 match keystroke.key.as_str() {
1856 "escape" => {
1857 self.filter_panel = None;
1858 return;
1859 }
1860 "enter" => {
1861 self.apply_filter_panel();
1862 return;
1863 }
1864 _ => {}
1865 }
1866 let mut edited = false;
1867 if let Some(panel) = &mut self.filter_panel {
1868 let input = panel.active_input_mut();
1869 match keystroke.key.as_str() {
1870 "backspace" => {
1871 input.backspace();
1872 edited = true;
1873 }
1874 "left" => input.move_left(),
1875 "right" => input.move_right(),
1876 _ => {
1877 if let Some(ch) = keystroke_to_char(keystroke) {
1878 input.insert_char(ch);
1879 edited = true;
1880 }
1881 }
1882 }
1883 }
1884 if edited {
1887 self.maybe_auto_apply();
1888 }
1889 return;
1890 }
1891 if self.context_menu.is_some() {
1892 if keystroke.key.as_str() == "escape" {
1893 self.context_menu = None;
1894 }
1895 return;
1896 }
1897 let shift = keystroke.modifiers.shift;
1898 match keystroke.key.as_str() {
1899 "up" if shift => self.extend_selection(0, -1),
1900 "down" if shift => self.extend_selection(0, 1),
1901 "left" if shift => self.extend_selection(-1, 0),
1902 "right" if shift => self.extend_selection(1, 0),
1903 "up" => self.move_selection(0, -1),
1904 "down" => self.move_selection(0, 1),
1905 "left" => self.move_selection(-1, 0),
1906 "right" => self.move_selection(1, 0),
1907 "escape" => {
1908 self.selection = Selection::None;
1909 self.range_anchor = None;
1910 self.range_active = None;
1911 }
1912 _ => {}
1913 }
1914 }
1915
1916 fn move_selection(&mut self, dx: i32, dy: i32) {
1917 let nrows = self.display_row_count() as i32;
1918 let ncols = self.data.columns.len() as i32;
1919 if nrows == 0 || ncols == 0 {
1920 return;
1921 }
1922 let last_row = nrows - 1;
1923 let last_col = ncols - 1;
1924 match self.selection {
1925 Selection::Cell(row, col) => {
1926 let nr = (row as i32 + dy).clamp(0, last_row) as usize;
1927 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
1928 self.selection = Selection::Cell(nr, nc);
1929 self.range_anchor = Some((nr, nc));
1930 self.range_active = Some((nr, nc));
1931 }
1932 Selection::Row(row) if dy != 0 => {
1933 let nr = (row as i32 + dy).clamp(0, last_row) as usize;
1934 self.selection = Selection::Row(nr);
1935 }
1936 Selection::Column(col) if dx != 0 => {
1937 let nc = (col as i32 + dx).clamp(0, last_col) as usize;
1938 self.selection = Selection::Column(nc);
1939 }
1940 _ => {
1941 self.selection = Selection::Cell(0, 0);
1942 self.range_anchor = Some((0, 0));
1943 self.range_active = Some((0, 0));
1944 }
1945 }
1946 }
1947
1948 fn extend_selection(&mut self, dx: i32, dy: i32) {
1952 let nrows = self.display_row_count() as i32;
1953 let ncols = self.data.columns.len() as i32;
1954 if nrows == 0 || ncols == 0 {
1955 return;
1956 }
1957 let last_row = nrows - 1;
1958 let last_col = ncols - 1;
1959
1960 if self.range_anchor.is_none() || self.range_active.is_none() {
1962 match self.selection {
1963 Selection::Cell(r, c) => {
1964 self.range_anchor = Some((r, c));
1965 self.range_active = Some((r, c));
1966 }
1967 Selection::CellRange(r1, c1, r2, c2) => {
1968 self.range_anchor = Some((r1, c1));
1969 self.range_active = Some((r2, c2));
1970 }
1971 _ => {
1972 self.range_anchor = Some((0, 0));
1973 self.range_active = Some((0, 0));
1974 self.selection = Selection::Cell(0, 0);
1975 }
1976 }
1977 }
1978
1979 let anchor = self.range_anchor.unwrap_or((0, 0));
1980 let active = self.range_active.unwrap_or(anchor);
1981 let nr = (active.0 as i32 + dy).clamp(0, last_row) as usize;
1982 let nc = (active.1 as i32 + dx).clamp(0, last_col) as usize;
1983 self.range_active = Some((nr, nc));
1984
1985 self.selection = if (nr, nc) == anchor {
1986 Selection::Cell(nr, nc)
1987 } else {
1988 Selection::CellRange(
1989 anchor.0.min(nr),
1990 anchor.1.min(nc),
1991 anchor.0.max(nr),
1992 anchor.1.max(nc),
1993 )
1994 };
1995 }
1996
1997 pub(crate) fn hit_test(&self, pos: Point<Pixels>) -> HitResult {
1998 let bounds = self.bounds;
1999 let (sx, sy) = (
2000 f32::from(self.scroll_handle.offset().x),
2001 f32::from(self.scroll_handle.offset().y),
2002 );
2003 let bw: f32 = bounds.size.width.into();
2004 let bh: f32 = bounds.size.height.into();
2005 let (mx, my) = self.max_scroll();
2006 if let Some(menu) = &self.context_menu {
2007 let cw = self.char_width;
2008 let x_rel = f32::from(pos.x);
2011 let y_rel = f32::from(pos.y);
2012 if let Some(idx) = menu_mod::hover_at(menu, x_rel, y_rel, cw) {
2013 return HitResult::ContextMenuItem(idx);
2014 }
2015 }
2016 if my > 0.0
2017 && f32::from(pos.x) >= bw - SCROLLBAR_SIZE
2018 && f32::from(pos.y) >= self.header_height
2019 {
2020 return HitResult::VerticalScrollbar;
2021 }
2022 if mx > 0.0
2023 && f32::from(pos.y) >= bh - SCROLLBAR_SIZE
2024 && f32::from(pos.x) >= self.row_header_width
2025 {
2026 return HitResult::HorizontalScrollbar;
2027 }
2028 let px = f32::from(pos.x);
2034 let py = f32::from(pos.y);
2035 if px < 0.0 || py < 0.0 || px > bw || py > bh {
2036 return HitResult::None;
2037 }
2038 self.hit_test_content(px, py, sx, sy)
2039 }
2040
2041 fn hit_test_content(&self, x: f32, y: f32, sx: f32, sy: f32) -> HitResult {
2042 if y < self.header_height {
2043 if x < self.row_header_width {
2044 return HitResult::Corner;
2045 }
2046 let col_x = x - self.row_header_width + sx;
2047 let mut acc = 0.0;
2048 for (i, col) in self.data.columns.iter().enumerate() {
2049 let right = acc + col.width;
2050 if i + 1 < self.data.columns.len() && col_x >= right - 5.0 && col_x <= right + 5.0 {
2051 return HitResult::ColumnBorder(i);
2052 }
2053 if col_x >= acc && col_x < right {
2054 if col_x >= right - 20.0 {
2055 return HitResult::SortButton(i);
2056 }
2057 return HitResult::ColumnHeader(i);
2058 }
2059 acc = right;
2060 }
2061 return HitResult::None;
2062 }
2063 if x < self.row_header_width {
2064 let row_y = y - self.header_height + sy;
2065 if row_y < 0.0 {
2066 return HitResult::None;
2067 }
2068 let row_idx = (row_y / self.row_height) as usize;
2069 if row_idx < self.display_row_count() {
2070 return HitResult::RowHeader(row_idx);
2071 }
2072 return HitResult::None;
2073 }
2074 let col_x = x - self.row_header_width + sx;
2075 let row_y = y - self.header_height + sy;
2076 if row_y < 0.0 {
2077 return HitResult::None;
2078 }
2079 let row_idx = (row_y / self.row_height) as usize;
2080 if row_idx >= self.display_row_count() {
2081 return HitResult::None;
2082 }
2083 let mut acc = 0.0;
2084 for (i, col) in self.data.columns.iter().enumerate() {
2085 if col_x >= acc && col_x < acc + col.width {
2086 return HitResult::Cell(row_idx, i);
2087 }
2088 acc += col.width;
2089 }
2090 HitResult::None
2091 }
2092
2093 #[must_use]
2094 pub fn wants_edge_scroll_tick(&self) -> bool {
2095 self.is_dragging
2096 }
2097}
2098
2099fn keystroke_to_char(k: &Keystroke) -> Option<char> {
2100 if k.modifiers.control || k.modifiers.platform || k.modifiers.alt {
2101 return None;
2102 }
2103 if let Some(key_char) = k.key_char.as_ref() {
2104 return key_char.chars().next();
2105 }
2106 if k.key.chars().count() == 1 {
2107 let c = k.key.chars().next()?;
2108 if k.modifiers.shift {
2109 Some(c.to_ascii_uppercase())
2110 } else {
2111 Some(c)
2112 }
2113 } else {
2114 None
2115 }
2116}
2117
2118#[cfg(test)]
2119#[allow(
2120 clippy::unwrap_used,
2121 clippy::expect_used,
2122 clippy::field_reassign_with_default
2123)]
2124mod tests {
2125 use super::*;
2126 use crate::data::{CellValue, Column, ColumnKind};
2127 use crate::grid::state::state_inner::{edge_scroll_speed, format_current_status};
2128
2129 fn input_with(text: &str, cursor: usize) -> TextInput {
2130 let mut p = TextInput::new(text.to_owned());
2131 p.cursor_chars = cursor;
2132 p
2133 }
2134
2135 #[test]
2136 fn text_input_new_cursors_at_char_count_not_bytes() {
2137 let p = TextInput::new("hé🙂".into());
2139 assert_eq!(p.cursor_chars, 3);
2140 assert_eq!(p.value.len(), 7);
2141 }
2142
2143 #[test]
2144 fn text_input_insert_emoji_at_start_does_not_panic() {
2145 let mut p = input_with("ab", 0);
2146 p.insert_char('\u{1F600}');
2147 assert_eq!(p.value, "\u{1F600}ab");
2148 assert_eq!(p.cursor_chars, 1);
2149 }
2150
2151 #[test]
2152 fn text_input_insert_in_middle_keeps_cursor_at_char_position() {
2153 let mut p = input_with("helloworld", 5);
2154 p.insert_char(' ');
2155 assert_eq!(p.value, "hello world");
2156 assert_eq!(p.cursor_chars, 6);
2157 }
2158
2159 #[test]
2160 fn text_input_backspace_at_zero_is_noop() {
2161 let mut p = input_with("abc", 0);
2162 p.backspace();
2163 assert_eq!(p.value, "abc");
2164 assert_eq!(p.cursor_chars, 0);
2165 }
2166
2167 #[test]
2168 fn text_input_backspace_removes_one_char_value() {
2169 let mut p = input_with("héx", 2);
2171 p.backspace();
2172 assert_eq!(p.value, "hx");
2173 assert_eq!(p.cursor_chars, 1);
2174 }
2175
2176 #[test]
2177 fn text_input_clamp_cursor_pulls_back_past_end() {
2178 let mut p = input_with("abc", 99);
2179 p.clamp_cursor();
2180 assert_eq!(p.cursor_chars, 3);
2181 }
2182
2183 #[test]
2184 fn text_input_move_left_and_right_respect_bounds() {
2185 let mut p = input_with("ab", 2);
2186 p.move_right();
2187 assert_eq!(p.cursor_chars, 2);
2188 p.move_left();
2189 p.move_left();
2190 p.move_left();
2191 assert_eq!(p.cursor_chars, 0);
2192 }
2193
2194 #[test]
2195 fn edge_scroll_speed_stops_outside_band() {
2196 assert_eq!(edge_scroll_speed(120.0), 0.0);
2198 assert_eq!(edge_scroll_speed(90.01), 0.0);
2199 assert_eq!(edge_scroll_speed(90.0), 4.0);
2201 assert_eq!(edge_scroll_speed(60.0), 4.0);
2202 assert_eq!(edge_scroll_speed(59.99), 8.0);
2203 assert_eq!(edge_scroll_speed(30.0), 8.0);
2205 assert_eq!(edge_scroll_speed(29.99), 16.0);
2206 assert_eq!(edge_scroll_speed(0.0), 16.0);
2208 assert_eq!(edge_scroll_speed(29.99), 16.0);
2209 }
2210
2211 #[test]
2212 fn edge_scroll_speed_caps_negative_runaway() {
2213 assert_eq!(edge_scroll_speed(-100.0), 16.0);
2215 assert_eq!(edge_scroll_speed(-1000.0), 16.0);
2216 }
2217
2218 #[allow(clippy::expect_used, clippy::unwrap_used)]
2226 #[test]
2227 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2228 fn grid_state_behavior_under_application() {
2229 gpui::Application::new().run(|cx| {
2230 let focus = cx.focus_handle();
2231
2232 let mut state = GridState::new(
2234 GridData::new(
2235 vec![Column::new("n", ColumnKind::Integer, 100.0)],
2236 vec![vec![CellValue::Integer(1)]],
2237 )
2238 .expect("rectangular"),
2239 crate::config::GridConfig::default(),
2240 focus.clone(),
2241 );
2242 let _ = format_current_status(&state);
2243 assert_eq!(state.selection, Selection::None);
2244
2245 state.last_mouse_pos = Some(Point {
2247 x: px(120.0),
2248 y: px(80.0),
2249 });
2250 let s = format_current_status(&state);
2251 assert!(s.contains("(120, 80)"), "missing positional, got: {s}");
2252
2253 let mut state = GridState::new(
2255 GridData::new(
2256 vec![Column::new("name", ColumnKind::Text, 100.0)],
2257 vec![
2258 vec![CellValue::Text("alpha".into())],
2259 vec![CellValue::Text("beeb".into())],
2260 vec![CellValue::Text("gamma".into())],
2261 ],
2262 )
2263 .expect("rectangular"),
2264 crate::config::GridConfig::default(),
2265 focus.clone(),
2266 );
2267 state.filters[0] = ColumnFilter {
2268 predicate: FilterPredicate::Text {
2269 op: TextOp::Contains,
2270 operand: "a".into(),
2271 },
2272 values: None,
2273 };
2274 state.toggle_sort(0);
2275 state.recompute();
2276 assert_eq!(state.display_indices.as_slice(), &[0, 2]);
2277 state.toggle_sort(0);
2278 state.recompute();
2279 assert_eq!(state.display_indices.as_slice(), &[2, 0]);
2280 state.filters[0] = ColumnFilter::default();
2281 state.toggle_sort(0);
2282 state.recompute();
2283 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
2284
2285 let mut state = GridState::new(
2287 GridData::new(
2288 vec![Column::new("v", ColumnKind::Integer, 80.0)],
2289 vec![vec![CellValue::Integer(1)]],
2290 )
2291 .expect("rectangular"),
2292 crate::config::GridConfig::default(),
2293 focus.clone(),
2294 );
2295 state.toggle_sort(0);
2296 assert_eq!(state.sort, Some((0, SortDirection::Ascending)));
2297 state.toggle_sort(0);
2298 assert_eq!(state.sort, Some((0, SortDirection::Descending)));
2299 state.toggle_sort(0);
2300 assert_eq!(state.sort, None);
2301
2302 let mut state = GridState::new(
2304 GridData::new(
2305 vec![
2306 Column::new("a", ColumnKind::Integer, 80.0),
2307 Column::new("b", ColumnKind::Integer, 80.0),
2308 ],
2309 vec![vec![CellValue::Integer(1), CellValue::Integer(2)]],
2310 )
2311 .expect("rectangular"),
2312 crate::config::GridConfig::default(),
2313 focus.clone(),
2314 );
2315 state.select_all();
2316 assert_eq!(state.selection, Selection::CellRange(0, 0, 0, 1));
2317
2318 let mut state = GridState::new(
2320 GridData::new(vec![Column::new("a", ColumnKind::Integer, 80.0)], vec![])
2321 .expect("rectangular"),
2322 crate::config::GridConfig::default(),
2323 focus.clone(),
2324 );
2325 state.select_all();
2326 assert_eq!(state.selection, Selection::None);
2327
2328 let mut state = GridState::new(
2330 GridData::new(
2331 vec![Column::new("v", ColumnKind::Decimal, 100.0)],
2332 vec![vec![CellValue::Decimal(1.234)]],
2333 )
2334 .expect("rectangular"),
2335 crate::config::GridConfig::default(),
2336 focus.clone(),
2337 );
2338 assert_eq!(state.resolved_formats[0].number.decimals, 2);
2339 let mut cfg = crate::config::GridConfig::default();
2340 cfg.column_overrides = vec![crate::config::ColumnOverride {
2341 number: Some(crate::config::NumberFormat {
2342 decimals: 6,
2343 ..Default::default()
2344 }),
2345 ..Default::default()
2346 }];
2347 state.set_config(cfg);
2348 assert_eq!(state.resolved_formats[0].number.decimals, 6);
2349
2350 let mut state = GridState::new(
2352 GridData::new(
2353 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2354 vec![vec![CellValue::Integer(1)]],
2355 )
2356 .expect("rectangular"),
2357 crate::config::GridConfig::default(),
2358 focus.clone(),
2359 );
2360 assert!(!state.wants_edge_scroll_tick());
2361 state.is_dragging = true;
2362 assert!(state.wants_edge_scroll_tick());
2363
2364 cx.quit();
2365 });
2366 }
2367
2368 #[allow(clippy::expect_used, clippy::unwrap_used)]
2369 #[test]
2370 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2371 fn context_menu_request_construction() {
2372 use crate::grid::context_menu::ContextMenuTarget;
2373
2374 gpui::Application::new().run(|cx| {
2375 let focus = cx.focus_handle();
2376
2377 let mut state = GridState::new(
2379 GridData::new(
2380 vec![
2381 Column::new("id", ColumnKind::Integer, 80.0),
2382 Column::new("name", ColumnKind::Text, 100.0),
2383 ],
2384 vec![
2385 vec![CellValue::Integer(1), CellValue::Text("alpha".into())],
2386 vec![CellValue::Integer(2), CellValue::Text("beta".into())],
2387 vec![CellValue::Integer(3), CellValue::Text("gamma".into())],
2388 ],
2389 )
2390 .expect("rectangular"),
2391 crate::config::GridConfig::default(),
2392 focus.clone(),
2393 );
2394 state.sort = Some((0, SortDirection::Descending));
2396 state.recompute();
2397 assert_eq!(state.display_indices.as_slice(), &[2, 1, 0]);
2398
2399 let target = ContextMenuTarget::Cell {
2401 display_row_index: 0,
2402 source_row_index: 2,
2403 column_index: 1,
2404 };
2405 let sel = Selection::Cell(0, 1);
2406 let req = state.build_context_menu_request(target, &sel);
2407 assert_eq!(req.target.column_index(), Some(1));
2408 let cells = req.selected_cells();
2409 assert_eq!(cells.len(), 1);
2410 assert_eq!(cells[0].source_row_index, 2);
2411 assert_eq!(cells[0].column_name, "name");
2412 assert_eq!(cells[0].value, CellValue::Text("gamma".into()));
2413 let rows = req.selected_rows();
2414 assert_eq!(rows.len(), 1);
2415 assert_eq!(rows[0].source_row_index, 2);
2416 assert_eq!(rows[0].value_by_name("id"), Some(&CellValue::Integer(3)));
2417
2418 let target = ContextMenuTarget::Cell {
2420 display_row_index: 0,
2421 source_row_index: 2,
2422 column_index: 0,
2423 };
2424 let sel = Selection::CellRange(0, 0, 1, 1);
2425 let req = state.build_context_menu_request(target, &sel);
2426 assert_eq!(req.selected_cell_count(), 4); let rows = req.selected_rows();
2428 assert_eq!(rows.len(), 2);
2429 assert_eq!(rows[0].source_row_index, 2);
2431 assert_eq!(rows[1].source_row_index, 1);
2432
2433 let target = ContextMenuTarget::RowHeader {
2435 display_row_index: 1,
2436 source_row_index: 1,
2437 };
2438 let sel = Selection::RowRange(0, 2);
2439 let req = state.build_context_menu_request(target, &sel);
2440 let rows = req.selected_rows();
2441 assert_eq!(rows.len(), 3);
2442 assert_eq!(rows[0].values.len(), 2);
2444 assert_eq!(req.selected_cell_count(), 6); let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
2450 let sel = Selection::Column(0);
2451 let req = state.build_context_menu_request(target, &sel);
2452 assert!(req.is_column_oriented());
2453 assert_eq!(req.selected_row_count(), 0);
2454 assert!(req.selected_rows().is_empty());
2455 assert_eq!(req.selected_cells().len(), 3); let empty_state = GridState::new(
2459 GridData::new(vec![Column::new("x", ColumnKind::Integer, 80.0)], vec![])
2460 .expect("rectangular"),
2461 crate::config::GridConfig::default(),
2462 focus.clone(),
2463 );
2464 let target = ContextMenuTarget::Cell {
2465 display_row_index: 0,
2466 source_row_index: 0,
2467 column_index: 0,
2468 };
2469 let req = empty_state.build_context_menu_request(target, &Selection::None);
2470 assert!(req.selected_cells().is_empty());
2471 assert!(req.selected_rows().is_empty());
2472
2473 cx.quit();
2474 });
2475 }
2476
2477 #[allow(clippy::expect_used, clippy::unwrap_used)]
2478 #[test]
2479 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2480 fn effective_selection_for_context_target() {
2481 gpui::Application::new().run(|cx| {
2482 let focus = cx.focus_handle();
2483 let mut state = GridState::new(
2484 GridData::new(
2485 vec![
2486 Column::new("a", ColumnKind::Integer, 80.0),
2487 Column::new("b", ColumnKind::Integer, 80.0),
2488 ],
2489 vec![
2490 vec![CellValue::Integer(1), CellValue::Integer(2)],
2491 vec![CellValue::Integer(3), CellValue::Integer(4)],
2492 ],
2493 )
2494 .expect("rectangular"),
2495 crate::config::GridConfig::default(),
2496 focus,
2497 );
2498
2499 state.selection = Selection::Cell(0, 0);
2501 let target = ContextMenuTarget::Cell {
2502 display_row_index: 1,
2503 source_row_index: 1,
2504 column_index: 1,
2505 };
2506 let eff = state.effective_selection_for_context_target(&target);
2507 assert_eq!(eff, Selection::Cell(1, 1));
2508
2509 state.selection = Selection::CellRange(0, 0, 1, 1);
2511 let target = ContextMenuTarget::Cell {
2512 display_row_index: 1,
2513 source_row_index: 1,
2514 column_index: 1,
2515 };
2516 let eff = state.effective_selection_for_context_target(&target);
2517 assert_eq!(eff, Selection::CellRange(0, 0, 1, 1));
2518
2519 state.selection = Selection::Cell(0, 0);
2521 let target = ContextMenuTarget::RowHeader {
2522 display_row_index: 1,
2523 source_row_index: 1,
2524 };
2525 let eff = state.effective_selection_for_context_target(&target);
2526 assert_eq!(eff, Selection::Row(1));
2527
2528 state.selection = Selection::RowRange(0, 1);
2530 let target = ContextMenuTarget::RowHeader {
2531 display_row_index: 1,
2532 source_row_index: 1,
2533 };
2534 let eff = state.effective_selection_for_context_target(&target);
2535 assert_eq!(eff, Selection::RowRange(0, 1));
2536
2537 state.selection = Selection::Cell(1, 1);
2539 let target = ContextMenuTarget::ColumnHeader { column_index: 0 };
2540 let eff = state.effective_selection_for_context_target(&target);
2541 assert_eq!(eff, Selection::Cell(1, 1));
2542
2543 cx.quit();
2544 });
2545 }
2546
2547 #[allow(clippy::expect_used, clippy::unwrap_used)]
2548 #[test]
2549 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2550 fn context_menu_target_from_hit_maps_correctly() {
2551 gpui::Application::new().run(|cx| {
2552 let focus = cx.focus_handle();
2553 let state = GridState::new(
2554 GridData::new(
2555 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2556 vec![vec![CellValue::Integer(1)], vec![CellValue::Integer(2)]],
2557 )
2558 .expect("rectangular"),
2559 crate::config::GridConfig::default(),
2560 focus,
2561 );
2562
2563 let t = state
2565 .context_menu_target_from_hit(HitResult::Cell(1, 0))
2566 .unwrap();
2567 assert_eq!(
2568 t,
2569 ContextMenuTarget::Cell {
2570 display_row_index: 1,
2571 source_row_index: 1,
2572 column_index: 0,
2573 }
2574 );
2575
2576 let t = state
2578 .context_menu_target_from_hit(HitResult::RowHeader(0))
2579 .unwrap();
2580 assert_eq!(
2581 t,
2582 ContextMenuTarget::RowHeader {
2583 display_row_index: 0,
2584 source_row_index: 0,
2585 }
2586 );
2587
2588 let t = state
2590 .context_menu_target_from_hit(HitResult::ColumnHeader(0))
2591 .unwrap();
2592 assert_eq!(t, ContextMenuTarget::ColumnHeader { column_index: 0 });
2593
2594 let t = state
2596 .context_menu_target_from_hit(HitResult::SortButton(0))
2597 .unwrap();
2598 assert_eq!(t, ContextMenuTarget::SortButton { column_index: 0 });
2599
2600 assert!(state
2602 .context_menu_target_from_hit(HitResult::VerticalScrollbar)
2603 .is_none());
2604 assert!(state
2605 .context_menu_target_from_hit(HitResult::None)
2606 .is_none());
2607
2608 cx.quit();
2609 });
2610 }
2611
2612 #[allow(clippy::expect_used, clippy::unwrap_used)]
2613 #[test]
2614 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2615 fn convert_context_menu_items_maps_variants() {
2616 use crate::grid::context_menu::ContextMenuItem;
2617
2618 let items = vec![
2619 ContextMenuItem::BuiltIn(MenuAction::SortAscending),
2620 ContextMenuItem::action("copy", "Copy value"),
2621 ContextMenuItem::separator(),
2622 ];
2623 let internal = GridState::convert_context_menu_items(items);
2624 assert!(matches!(
2625 internal[0],
2626 MenuItem::Action(MenuAction::SortAscending)
2627 ));
2628 assert!(
2629 matches!(&internal[1], MenuItem::Custom { id, label } if id == "copy" && label == "Copy value")
2630 );
2631 assert!(matches!(internal[2], MenuItem::Separator));
2632 }
2633
2634 #[allow(clippy::expect_used, clippy::unwrap_used)]
2635 #[test]
2636 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2637 fn execute_custom_context_menu_action_invokes_provider() {
2638 use crate::grid::context_menu::{
2639 ContextMenuProvider, ContextMenuProviderHandle, ContextMenuRequest,
2640 };
2641 use std::sync::{Arc, Mutex};
2642
2643 #[derive(Default)]
2644 struct TestProvider {
2645 last_action: Arc<Mutex<Option<String>>>,
2646 }
2647 impl ContextMenuProvider for TestProvider {
2648 fn menu_items(&self, _request: &ContextMenuRequest) -> Vec<ContextMenuItem> {
2649 vec![ContextMenuItem::action("test", "Test")]
2650 }
2651 fn on_action(
2652 &self,
2653 action_id: &str,
2654 _request: &ContextMenuRequest,
2655 _state: &mut GridState,
2656 _cx: &mut gpui::App,
2657 ) {
2658 *self.last_action.lock().unwrap() = Some(action_id.to_string());
2659 }
2660 }
2661
2662 gpui::Application::new().run(|cx| {
2663 let focus = cx.focus_handle();
2664 let mut state = GridState::new(
2665 GridData::new(
2666 vec![Column::new("a", ColumnKind::Integer, 80.0)],
2667 vec![vec![CellValue::Integer(1)]],
2668 )
2669 .expect("rectangular"),
2670 crate::config::GridConfig::default(),
2671 focus,
2672 );
2673
2674 let last = Arc::new(Mutex::new(None));
2675 state.context_menu_provider = Some(ContextMenuProviderHandle::new(TestProvider {
2676 last_action: last.clone(),
2677 }));
2678
2679 let target = ContextMenuTarget::Cell {
2680 display_row_index: 0,
2681 source_row_index: 0,
2682 column_index: 0,
2683 };
2684 let request = state.build_context_menu_request(target, &Selection::Cell(0, 0));
2685 state.execute_custom_context_menu_action(
2686 PendingCustomContextMenuAction {
2687 id: "test".into(),
2688 request,
2689 },
2690 cx,
2691 );
2692 assert_eq!(*last.lock().unwrap(), Some("test".to_string()));
2693 assert!(state.context_menu.is_none());
2694
2695 cx.quit();
2696 });
2697 }
2698
2699 #[test]
2700 fn filter_panel_to_filter_with_all_checked_has_no_value_set() {
2701 let panel = FilterPanel {
2702 col: 0,
2703 anchor: Point {
2704 x: px(0.0),
2705 y: px(0.0),
2706 },
2707 kind: ColumnKind::Text,
2708 search: TextInput::default(),
2709 op_index: 0,
2710 op_menu_open: false,
2711 operand_a: TextInput::default(),
2712 operand_b: TextInput::default(),
2713 focus: FilterInput::Search,
2714 auto_apply: true,
2715 distinct: vec![
2716 FilterValueRow {
2717 label: "alpha".into(),
2718 checked: true,
2719 },
2720 FilterValueRow {
2721 label: "beta".into(),
2722 checked: true,
2723 },
2724 ],
2725 };
2726 let f = panel.to_filter();
2727 assert!(f.values.is_none(), "all checked => no value allow-list");
2728 assert!(
2729 !f.is_active(),
2730 "default predicate + all checked => inactive"
2731 );
2732 }
2733
2734 #[test]
2735 fn filter_panel_to_filter_with_unchecked_value_builds_allow_set() {
2736 let panel = FilterPanel {
2737 col: 0,
2738 anchor: Point {
2739 x: px(0.0),
2740 y: px(0.0),
2741 },
2742 kind: ColumnKind::Text,
2743 search: TextInput::default(),
2744 op_index: 0,
2745 op_menu_open: false,
2746 operand_a: TextInput::default(),
2747 operand_b: TextInput::default(),
2748 focus: FilterInput::Search,
2749 auto_apply: true,
2750 distinct: vec![
2751 FilterValueRow {
2752 label: "alpha".into(),
2753 checked: true,
2754 },
2755 FilterValueRow {
2756 label: "beta".into(),
2757 checked: false,
2758 },
2759 ],
2760 };
2761 let f = panel.to_filter();
2762 assert!(f.is_active(), "unchecked value => active filter");
2763 let set = f.values.expect("should have a value set");
2764 assert!(set.contains("alpha"));
2765 assert!(!set.contains("beta"));
2766 }
2767
2768 #[test]
2769 fn filter_panel_visible_indices_respects_search() {
2770 let panel = FilterPanel {
2771 col: 0,
2772 anchor: Point {
2773 x: px(0.0),
2774 y: px(0.0),
2775 },
2776 kind: ColumnKind::Text,
2777 search: TextInput::new("al".into()),
2778 op_index: 0,
2779 op_menu_open: false,
2780 operand_a: TextInput::default(),
2781 operand_b: TextInput::default(),
2782 focus: FilterInput::Search,
2783 auto_apply: true,
2784 distinct: vec![
2785 FilterValueRow {
2786 label: "alpha".into(),
2787 checked: true,
2788 },
2789 FilterValueRow {
2790 label: "beta".into(),
2791 checked: true,
2792 },
2793 FilterValueRow {
2794 label: "gamma".into(),
2795 checked: true,
2796 },
2797 ],
2798 };
2799 let vis = panel.visible_indices();
2800 assert_eq!(vis, vec![0], "search 'al' matches only alpha");
2801 }
2802
2803 #[test]
2804 fn filter_panel_all_checked_ignores_search() {
2805 let mut panel = FilterPanel {
2806 col: 0,
2807 anchor: Point {
2808 x: px(0.0),
2809 y: px(0.0),
2810 },
2811 kind: ColumnKind::Text,
2812 search: TextInput::new("al".into()),
2813 op_index: 0,
2814 op_menu_open: false,
2815 operand_a: TextInput::default(),
2816 operand_b: TextInput::default(),
2817 focus: FilterInput::Search,
2818 auto_apply: true,
2819 distinct: vec![
2820 FilterValueRow {
2821 label: "alpha".into(),
2822 checked: true,
2823 },
2824 FilterValueRow {
2825 label: "beta".into(),
2826 checked: false,
2827 },
2828 FilterValueRow {
2829 label: "gamma".into(),
2830 checked: true,
2831 },
2832 ],
2833 };
2834 assert!(
2837 !panel.all_checked(),
2838 "beta is unchecked, so not all values are checked (search is irrelevant)"
2839 );
2840
2841 panel.search = TextInput::new("zzz".into());
2843 for row in &mut panel.distinct {
2844 row.checked = true;
2845 }
2846 assert!(
2847 panel.all_checked(),
2848 "all values checked -> Select All stays checked regardless of empty search"
2849 );
2850 }
2851
2852 #[allow(clippy::expect_used, clippy::unwrap_used)]
2853 #[test]
2854 #[ignore = "requires gpui::Application which must run on the OS main thread; can only be executed under a custom main harness"]
2855 fn filter_panel_open_apply_clear_state_flow() {
2856 gpui::Application::new().run(|cx| {
2857 let focus = cx.focus_handle();
2858 let mut state = GridState::new(
2859 GridData::new(
2860 vec![Column::new("name", ColumnKind::Text, 100.0)],
2861 vec![
2862 vec![CellValue::Text("alpha".into())],
2863 vec![CellValue::Text("beta".into())],
2864 vec![CellValue::Text("gamma".into())],
2865 ],
2866 )
2867 .expect("rectangular"),
2868 crate::config::GridConfig::default(),
2869 focus,
2870 );
2871
2872 let anchor = Point {
2874 x: px(50.0),
2875 y: px(20.0),
2876 };
2877 state.open_filter_panel(0, Some(anchor));
2878 let panel = state.filter_panel.as_ref().expect("panel should be open");
2879 assert_eq!(panel.col, 0);
2880 assert_eq!(panel.anchor, anchor);
2881 assert_eq!(panel.distinct.len(), 3);
2882 assert!(
2883 panel.distinct.iter().all(|r| r.checked),
2884 "all checked by default"
2885 );
2886 assert!(panel.auto_apply, "auto_apply defaults to true");
2887 assert_eq!(panel.kind, ColumnKind::Text);
2888
2889 state.toggle_filter_value(1);
2891 state.apply_filter_panel();
2892 assert_eq!(
2893 state.display_indices.as_slice(),
2894 &[0, 2],
2895 "beta should be filtered out"
2896 );
2897
2898 state.clear_filter_panel();
2900 assert_eq!(
2901 state.display_indices.as_slice(),
2902 &[0, 1, 2],
2903 "all rows visible after clear"
2904 );
2905 assert!(
2906 state.filters[0] == ColumnFilter::default(),
2907 "filter reset to default"
2908 );
2909
2910 state.open_filter_panel(0, Some(anchor));
2912 let panel = state.filter_panel.as_mut().expect("panel open");
2913 panel.op_index = 1; panel.operand_a = TextInput::new("a".into());
2915 state.apply_filter_panel();
2916 assert_eq!(
2917 state.display_indices.as_slice(),
2918 &[0, 2],
2919 "contains 'a' matches alpha and gamma"
2920 );
2921
2922 state.clear_filter_panel();
2924 assert_eq!(state.display_indices.as_slice(), &[0, 1, 2]);
2925
2926 cx.quit();
2927 });
2928 }
2929}