1use crate::config::GridConfig;
7use crate::data::GridData;
8use crate::filter::{ColumnFilter, FilterPredicate};
9use crate::grid::context_menu::{
10 ContextMenuProvider, ContextMenuProviderHandle, PendingCustomContextMenuAction,
11};
12use crate::grid::paint::{paint_grid, paint_status_bar, PaintData, StatusBarData};
13use crate::grid::state::state_inner;
14use crate::grid::state::{FilterInput, GridState, EDGE_SCROLL_TICK_MS};
15use crate::grid::theme::{GridTheme, GridThemePair};
16use crate::grid::{menu, HitResult, MenuItem, SortDirection};
17use crate::pivot::config::PivotConfig;
18use crate::pivot::context_menu::{PivotContextMenuProvider, PivotContextMenuProviderHandle};
19use crate::pivot::sidebar::PivotSidebar;
20use crate::pivot::state::{
21 PivotSaveConfigHandler, PivotState, DEFAULT_PIVOT_COLUMN_WIDTH, DEFAULT_PIVOT_ROW_HEIGHT,
22 DEFAULT_PIVOT_SIDEBAR_WIDTH,
23};
24use crate::pivot::widget::PivotGrid;
25
26use gpui::prelude::FluentBuilder;
27use gpui::{
28 anchored, canvas, deferred, div, point, pulsating_between, px, relative, Animation,
29 AnimationExt, App, AppContext, Context, Corner, Div, Entity, FocusHandle, Focusable,
30 InteractiveElement, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
31 MouseUpEvent, ParentElement, Render, ScrollWheelEvent, StatefulInteractiveElement, Styled,
32 Window,
33};
34use gpui_ui_kit::pane_divider::{CollapseDirection, PaneDivider, PaneDividerTheme};
35use std::sync::Arc;
36
37const CONTEXT_MENU_PRIORITY: usize = 1_000_000;
44const MIN_PIVOT_SIDEBAR_WIDTH: f32 = 180.0;
45const MAX_PIVOT_SIDEBAR_WIDTH: f32 = 480.0;
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub enum GridTab {
50 #[default]
52 Grid,
53 Pivot,
55}
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub enum PivotSidebarPosition {
60 #[default]
62 Left,
63 Right,
65}
66
67#[derive(Clone, Copy)]
68struct PivotSidebarDrag {
69 start_x: f32,
70 start_width: f32,
71}
72
73pub(crate) struct PivotParts {
75 pub(crate) state: Entity<PivotState>,
76 grid: Entity<PivotGrid>,
77 sidebar: Entity<PivotSidebar>,
78}
79
80pub struct SqllyDataTable {
82 pub state: Entity<GridState>,
83 pub(crate) pivot: Option<PivotParts>,
86 active_tab: GridTab,
88 pivot_locked: bool,
91 pivot_status: Option<String>,
92 pivot_sidebar_position: PivotSidebarPosition,
93 pivot_sidebar_collapsed: bool,
94 pivot_sidebar_width: f32,
95 pivot_sidebar_drag: Option<PivotSidebarDrag>,
96 follow_system_appearance: bool,
100 appearance_subscription: Option<gpui::Subscription>,
104}
105
106impl SqllyDataTable {
107 #[must_use]
109 pub fn new(state: Entity<GridState>) -> Self {
110 Self {
111 state,
112 pivot: None,
113 active_tab: GridTab::Grid,
114 pivot_locked: false,
115 pivot_status: None,
116 pivot_sidebar_position: PivotSidebarPosition::Left,
117 pivot_sidebar_collapsed: false,
118 pivot_sidebar_width: DEFAULT_PIVOT_SIDEBAR_WIDTH,
119 pivot_sidebar_drag: None,
120 follow_system_appearance: true,
121 appearance_subscription: None,
122 }
123 }
124
125 #[must_use]
127 pub fn builder(data: GridData) -> SqllyDataTableBuilder {
128 SqllyDataTableBuilder {
129 data,
130 config: GridConfig::default(),
131 context_menu_provider: None,
132 theme: None,
133 theme_family: None,
134 debug_bar: false,
135 grouped_column: None,
136 pivot: None,
137 pivot_context_menu_provider: None,
138 pivot_save_config_handler: None,
139 pivot_sidebar_position: PivotSidebarPosition::Left,
140 pivot_sidebar_collapsed: false,
141 pivot_sidebar_width: DEFAULT_PIVOT_SIDEBAR_WIDTH,
142 pivot_row_height: DEFAULT_PIVOT_ROW_HEIGHT,
143 pivot_column_width: DEFAULT_PIVOT_COLUMN_WIDTH,
144 }
145 }
146
147 #[must_use]
151 pub fn pivot_state(&self) -> Option<&Entity<PivotState>> {
152 self.pivot.as_ref().map(|p| &p.state)
153 }
154
155 #[must_use]
157 pub fn active_tab(&self) -> GridTab {
158 self.active_tab
159 }
160
161 pub fn set_theme_family(
168 &mut self,
169 family: GridThemePair,
170 window: &Window,
171 cx: &mut Context<Self>,
172 ) {
173 self.follow_system_appearance = true;
174 let appearance = window.appearance();
175 self.state.update(cx, |s, cx| {
176 s.theme = family.for_appearance(appearance);
177 s.theme_family = family;
178 cx.notify();
179 });
180 cx.notify();
181 }
182
183 #[must_use]
185 pub fn pivot_locked(&self) -> bool {
186 self.pivot_locked
187 }
188
189 #[must_use]
191 pub fn pivot_sidebar_position(&self) -> PivotSidebarPosition {
192 self.pivot_sidebar_position
193 }
194
195 pub fn set_pivot_sidebar_position(&mut self, position: PivotSidebarPosition) {
197 self.pivot_sidebar_position = position;
198 self.pivot_sidebar_drag = None;
199 }
200
201 #[must_use]
203 pub fn pivot_sidebar_collapsed(&self) -> bool {
204 self.pivot_sidebar_collapsed
205 }
206
207 pub fn set_pivot_sidebar_collapsed(&mut self, collapsed: bool) {
209 self.pivot_sidebar_collapsed = collapsed;
210 self.pivot_sidebar_drag = None;
211 }
212
213 #[must_use]
215 pub fn pivot_sidebar_width(&self) -> f32 {
216 self.pivot_sidebar_width
217 }
218
219 pub fn set_pivot_sidebar_width(&mut self, width: f32) {
221 self.pivot_sidebar_width = width.clamp(MIN_PIVOT_SIDEBAR_WIDTH, MAX_PIVOT_SIDEBAR_WIDTH);
222 }
223
224 pub fn set_pivot_locked(&mut self, locked: bool, status: Option<String>) {
229 self.pivot_locked = locked;
230 self.pivot_status = locked.then_some(status).flatten();
231 if locked {
232 self.active_tab = GridTab::Grid;
233 }
234 }
235
236 pub fn set_active_tab(&mut self, tab: GridTab, cx: &mut App) {
241 if tab == GridTab::Pivot {
242 if self.pivot.is_none() || self.pivot_locked {
243 return;
244 }
245 self.sync_pivot_source(cx);
246 }
247 self.active_tab = tab;
248 }
249
250 pub fn enable_pivot(&mut self, config: PivotConfig, cx: &mut App) {
254 if let Some(parts) = &self.pivot {
255 parts.state.update(cx, |s, cx| {
256 s.config = config;
257 s.recompute();
258 cx.notify();
259 });
260 return;
261 }
262 self.pivot = Some(build_pivot_parts(
263 &self.state,
264 config,
265 None,
266 None,
267 DEFAULT_PIVOT_ROW_HEIGHT,
268 DEFAULT_PIVOT_COLUMN_WIDTH,
269 cx,
270 ));
271 }
272
273 pub fn disable_pivot(&mut self) {
275 self.pivot = None;
276 self.active_tab = GridTab::Grid;
277 }
278
279 pub fn set_pivot_context_menu_provider(
284 &mut self,
285 provider: impl PivotContextMenuProvider + 'static,
286 cx: &mut App,
287 ) {
288 if let Some(parts) = &self.pivot {
289 parts.state.update(cx, |s, _cx| {
290 s.set_context_menu_provider(provider);
291 });
292 }
293 }
294
295 pub fn set_pivot_save_config(
302 &mut self,
303 handler: impl Fn(&PivotConfig, &mut App) + 'static,
304 cx: &mut App,
305 ) {
306 if let Some(parts) = &self.pivot {
307 parts.state.update(cx, |s, cx| {
308 s.on_save_config(handler);
309 cx.notify();
310 });
311 }
312 }
313
314 pub fn clear_pivot_save_config(&mut self, cx: &mut App) {
317 if let Some(parts) = &self.pivot {
318 parts.state.update(cx, |s, cx| {
319 s.clear_save_config_handler();
320 cx.notify();
321 });
322 }
323 }
324
325 fn sync_pivot_source(&self, cx: &mut App) {
328 let Some(parts) = &self.pivot else {
329 return;
330 };
331 let (columns, rows) = {
332 let s = self.state.read(cx);
333 (s.data.columns.clone(), Arc::clone(&s.data_rows))
334 };
335 parts.state.update(cx, |ps, cx| {
336 if ps.source_differs(&rows) {
337 ps.set_source(columns, rows);
338 cx.notify();
339 }
340 });
341 }
342}
343
344fn build_pivot_parts(
346 grid_state: &Entity<GridState>,
347 config: PivotConfig,
348 menu_provider: Option<PivotContextMenuProviderHandle>,
349 save_config_handler: Option<PivotSaveConfigHandler>,
350 row_height: f32,
351 column_width: f32,
352 cx: &mut App,
353) -> PivotParts {
354 let (columns, rows, formats, key_bindings, theme) = {
355 let s = grid_state.read(cx);
356 (
357 s.data.columns.clone(),
358 Arc::clone(&s.data_rows),
359 s.resolved_formats.as_ref().clone(),
360 s.config.key_bindings.clone(),
361 s.theme.clone(),
362 )
363 };
364 let focus = cx.focus_handle();
365 let state = cx.new(|_| {
366 let mut ps = PivotState::new(columns, rows, formats, config, key_bindings, focus);
367 ps.theme = theme;
368 ps.context_menu_provider = menu_provider;
369 ps.save_config_handler = save_config_handler;
370 ps.set_row_height(row_height);
371 ps.set_column_width(column_width);
372 ps
373 });
374 let grid = cx.new(|_| PivotGrid::new(state.clone()));
375 let sidebar = cx.new(|_| PivotSidebar::new(state.clone()));
376 PivotParts {
377 state,
378 grid,
379 sidebar,
380 }
381}
382
383pub struct SqllyDataTableBuilder {
385 data: GridData,
386 config: GridConfig,
387 context_menu_provider: Option<ContextMenuProviderHandle>,
388 theme: Option<GridTheme>,
389 theme_family: Option<GridThemePair>,
390 debug_bar: bool,
391 grouped_column: Option<usize>,
392 pivot: Option<PivotConfig>,
393 pivot_context_menu_provider: Option<PivotContextMenuProviderHandle>,
394 pivot_save_config_handler: Option<PivotSaveConfigHandler>,
395 pivot_sidebar_position: PivotSidebarPosition,
396 pivot_sidebar_collapsed: bool,
397 pivot_sidebar_width: f32,
398 pivot_row_height: f32,
399 pivot_column_width: f32,
400}
401
402impl SqllyDataTableBuilder {
403 #[must_use]
405 pub fn config(mut self, config: GridConfig) -> Self {
406 self.config = config;
407 self
408 }
409
410 #[must_use]
413 pub fn theme(mut self, theme: GridTheme) -> Self {
414 self.theme = Some(theme);
415 self
416 }
417
418 #[must_use]
425 pub fn theme_family(mut self, family: GridThemePair) -> Self {
426 self.theme_family = Some(family);
427 self
428 }
429
430 #[must_use]
437 pub fn context_menu_provider(mut self, provider: impl ContextMenuProvider + 'static) -> Self {
438 self.context_menu_provider = Some(ContextMenuProviderHandle::new(provider));
439 self
440 }
441
442 #[must_use]
446 pub fn debug_bar(mut self, enabled: bool) -> Self {
447 self.debug_bar = enabled;
448 self
449 }
450
451 #[must_use]
454 pub fn group_by_column(mut self, column: usize) -> Self {
455 self.grouped_column = Some(column);
456 self
457 }
458
459 #[must_use]
465 pub fn pivot(mut self, config: PivotConfig) -> Self {
466 self.pivot = Some(config);
467 self
468 }
469
470 #[must_use]
472 pub fn pivot_sidebar_position(mut self, position: PivotSidebarPosition) -> Self {
473 self.pivot_sidebar_position = position;
474 self
475 }
476
477 #[must_use]
479 pub fn pivot_sidebar_collapsed(mut self, collapsed: bool) -> Self {
480 self.pivot_sidebar_collapsed = collapsed;
481 self
482 }
483
484 #[must_use]
486 pub fn pivot_sidebar_width(mut self, width: f32) -> Self {
487 self.pivot_sidebar_width = width.clamp(MIN_PIVOT_SIDEBAR_WIDTH, MAX_PIVOT_SIDEBAR_WIDTH);
488 self
489 }
490
491 #[must_use]
493 pub fn pivot_row_height(mut self, height: f32) -> Self {
494 if height.is_finite() {
495 self.pivot_row_height = height.max(crate::pivot::state::MIN_PIVOT_ROW_HEIGHT);
496 }
497 self
498 }
499
500 #[must_use]
502 pub fn pivot_column_width(mut self, width: f32) -> Self {
503 if width.is_finite() {
504 self.pivot_column_width = width.max(crate::pivot::state::MIN_PIVOT_COLUMN_WIDTH);
505 }
506 self
507 }
508
509 #[must_use]
515 pub fn pivot_context_menu_provider(
516 mut self,
517 provider: impl PivotContextMenuProvider + 'static,
518 ) -> Self {
519 self.pivot_context_menu_provider = Some(PivotContextMenuProviderHandle::new(provider));
520 self
521 }
522
523 #[must_use]
531 pub fn pivot_save_config(mut self, handler: impl Fn(&PivotConfig, &mut App) + 'static) -> Self {
532 self.pivot_save_config_handler = Some(std::rc::Rc::new(handler));
533 self
534 }
535
536 pub fn build(self, cx: &mut App) -> SqllyDataTable {
538 let focus = cx.focus_handle();
539 let provider = self.context_menu_provider;
540 let theme_override = self.theme;
541 let theme_family = self.theme_family;
542 let debug_bar = self.debug_bar;
543 let grouped_column = self.grouped_column;
544 let pivot_config = self.pivot;
545 let pivot_sidebar_position = self.pivot_sidebar_position;
546 let pivot_sidebar_collapsed = self.pivot_sidebar_collapsed;
547 let pivot_sidebar_width = self.pivot_sidebar_width;
548 let pivot_row_height = self.pivot_row_height;
549 let pivot_column_width = self.pivot_column_width;
550 let follow_system_appearance = theme_override.is_none();
551 let state = cx.new(|cx| {
552 let mut s = GridState::new(self.data, self.config, focus.clone());
553 s.context_menu_provider = provider;
554 s.debug_bar_enabled = debug_bar;
555 s.set_grouped_column(grouped_column);
556 s.self_weak = Some(cx.weak_entity());
557 if let Some(family) = theme_family {
558 s.theme = family.light.clone();
559 s.theme_family = family;
560 }
561 if let Some(theme) = theme_override {
562 s.theme = theme;
563 }
564 s
565 });
566 let pivot_menu_provider = self.pivot_context_menu_provider;
567 let pivot_save_config_handler = self.pivot_save_config_handler;
568 let pivot = pivot_config.map(|cfg| {
569 build_pivot_parts(
570 &state,
571 cfg,
572 pivot_menu_provider,
573 pivot_save_config_handler,
574 pivot_row_height,
575 pivot_column_width,
576 cx,
577 )
578 });
579 SqllyDataTable {
580 state,
581 pivot,
582 active_tab: GridTab::Grid,
583 pivot_locked: false,
584 pivot_status: None,
585 pivot_sidebar_position,
586 pivot_sidebar_collapsed,
587 pivot_sidebar_width,
588 pivot_sidebar_drag: None,
589 follow_system_appearance,
590 appearance_subscription: None,
591 }
592 }
593}
594
595impl Focusable for SqllyDataTable {
596 fn focus_handle(&self, cx: &App) -> FocusHandle {
597 self.state.read(cx).focus_handle.clone()
598 }
599}
600
601impl Render for SqllyDataTable {
602 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
603 if self.follow_system_appearance {
615 let appearance = window.appearance();
616 let resolved = self.state.read(cx).theme_family.for_appearance(appearance);
617 if self.state.read(cx).theme != resolved {
618 self.state.update(cx, |s, cx| {
619 s.theme = resolved;
620 cx.notify();
621 });
622 }
623 if self.appearance_subscription.is_none() {
624 let state_appearance = self.state.clone();
625 self.appearance_subscription =
626 Some(window.observe_window_appearance(move |window, cx| {
627 let appearance = window.appearance();
628 state_appearance.update(cx, |s, cx| {
629 let resolved = s.theme_family.for_appearance(appearance);
630 if s.theme != resolved {
631 s.theme = resolved;
632 cx.notify();
633 }
634 });
635 }));
636 }
637 }
638
639 if let Some(parts) = &self.pivot {
644 let grid_theme = self.state.read(cx).theme.clone();
645 let sidebar_width = self.pivot_sidebar_width;
646 parts.state.update(cx, |s, cx| {
647 let mut dirty = false;
648 if s.theme != grid_theme {
649 s.theme = grid_theme;
650 dirty = true;
651 }
652 if (s.sidebar_width - sidebar_width).abs() > 0.5 {
653 s.sidebar_width = sidebar_width;
654 dirty = true;
655 }
656 if dirty {
657 cx.notify();
658 }
659 });
660 }
661
662 if let Some(parts) = &self.pivot {
667 let drill = parts.state.update(cx, |s, _cx| s.take_pending_drill_down());
668 if let Some(filter_sets) = drill {
669 self.state.update(cx, |g, cx| {
670 if g.window.is_none() {
674 for filter in &mut g.filters {
675 *filter = ColumnFilter::default();
676 }
677 for (field, values) in filter_sets {
678 if let Some(slot) = g.filters.get_mut(field) {
679 *slot = ColumnFilter {
680 predicate: FilterPredicate::None,
681 values: Some(values),
682 };
683 }
684 }
685 g.recompute();
686 }
687 cx.notify();
688 });
689 self.active_tab = GridTab::Grid;
690 let focus = self.state.read(cx).focus_handle.clone();
691 window.focus(&focus);
692 cx.notify();
693 }
694 }
695
696 let grid_view = self.render_grid_view(cx);
697
698 let Some(parts) = &self.pivot else {
699 return div().size_full().child(grid_view);
700 };
701
702 let theme = self.state.read(cx).theme.clone();
703 let tab = |label: String,
704 this_tab: GridTab,
705 active: bool,
706 locked: bool,
707 cx: &mut Context<Self>| {
708 div()
709 .px(px(14.0))
710 .py(px(5.0))
711 .text_size(px(13.0))
712 .when(!locked, |tab| tab.cursor_pointer())
713 .when(locked, |tab| tab.opacity(0.55))
714 .bg(if active { theme.bg } else { theme.header_bg })
715 .text_color(if active {
716 theme.header_fg
717 } else {
718 theme.muted_text
719 })
720 .border_b_2()
721 .border_color(if active {
722 theme.sort_indicator
723 } else {
724 theme.header_bg
725 })
726 .child(label)
727 .when(!locked, |tab| {
728 tab.on_mouse_down(
729 MouseButton::Left,
730 cx.listener(move |this, _e: &MouseDownEvent, window, cx| {
731 if this.active_tab == this_tab {
732 return;
733 }
734 this.set_active_tab(this_tab, cx);
735 match this_tab {
737 GridTab::Grid => {
738 let focus = this.state.read(cx).focus_handle.clone();
739 window.focus(&focus);
740 }
741 GridTab::Pivot => {
742 if let Some(p) = &this.pivot {
743 let focus = p.state.read(cx).focus_handle.clone();
744 window.focus(&focus);
745 }
746 }
747 }
748 cx.notify();
749 }),
750 )
751 })
752 };
753
754 let pivot_label = self
755 .pivot_status
756 .as_deref()
757 .map_or_else(|| "Pivot".to_string(), |status| format!("Pivot {status}"));
758
759 let tab_bar = div()
760 .flex()
761 .flex_row()
762 .flex_none()
763 .bg(theme.header_bg)
764 .border_b_1()
765 .border_color(theme.grid_line)
766 .child(tab(
767 "Grid".to_string(),
768 GridTab::Grid,
769 self.active_tab == GridTab::Grid,
770 false,
771 cx,
772 ))
773 .child(tab(
774 pivot_label,
775 GridTab::Pivot,
776 self.active_tab == GridTab::Pivot,
777 self.pivot_locked,
778 cx,
779 ));
780
781 let content: gpui::AnyElement = match self.active_tab {
782 GridTab::Grid => grid_view.into_any_element(),
783 GridTab::Pivot => {
784 let position = self.pivot_sidebar_position;
785 let collapsed = self.pivot_sidebar_collapsed;
786 let panel = div()
787 .w(px(self.pivot_sidebar_width))
788 .h_full()
789 .flex_none()
790 .child(parts.sidebar.clone());
791 let pivot_grid = div().flex_1().min_w_0().child(parts.grid.clone());
792 let direction = match position {
793 PivotSidebarPosition::Left => CollapseDirection::Left,
794 PivotSidebarPosition::Right => CollapseDirection::Right,
795 };
796 let divider_theme = PaneDividerTheme {
797 background: theme.header_bg.into(),
798 background_hover: theme.menu_hover_bg.into(),
799 background_collapsed: theme.menu_bg.into(),
800 foreground: theme.muted_text.into(),
801 foreground_hover: theme.header_fg.into(),
802 border: theme.grid_line.into(),
803 };
804 let table_toggle = cx.entity().clone();
805 let table_drag = cx.entity().clone();
806 let divider = PaneDivider::vertical("pivot-sidebar-divider", direction)
807 .label("Pivot")
808 .collapsed(collapsed)
809 .theme(divider_theme)
810 .on_toggle(move |collapsed, _window, cx| {
811 table_toggle.update(cx, |table, cx| {
812 table.set_pivot_sidebar_collapsed(collapsed);
813 cx.notify();
814 });
815 })
816 .on_drag_start(move |start_x, _window, cx| {
817 table_drag.update(cx, |table, cx| {
818 table.pivot_sidebar_drag = Some(PivotSidebarDrag {
819 start_x,
820 start_width: table.pivot_sidebar_width,
821 });
822 cx.notify();
823 });
824 });
825
826 let mut pivot_view = div().flex().flex_row().size_full();
827 pivot_view = match position {
828 PivotSidebarPosition::Left => pivot_view
829 .children((!collapsed).then_some(panel))
830 .child(divider)
831 .child(pivot_grid),
832 PivotSidebarPosition::Right => pivot_view
833 .child(pivot_grid)
834 .child(divider)
835 .children((!collapsed).then_some(panel)),
836 };
837
838 pivot_view.into_any_element()
839 }
840 };
841
842 let mut root = div()
843 .flex()
844 .flex_col()
845 .size_full()
846 .child(tab_bar)
847 .child(div().flex_1().min_h_0().child(content));
848
849 if let Some(drag) = self.pivot_sidebar_drag {
850 let position = self.pivot_sidebar_position;
851 let table_move = cx.entity().clone();
852 let table_up = cx.entity().clone();
853 let table_up_out = cx.entity().clone();
854 root = root
855 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
856 let current_x: f32 = event.position.x.into();
857 let delta = match position {
858 PivotSidebarPosition::Left => current_x - drag.start_x,
859 PivotSidebarPosition::Right => drag.start_x - current_x,
860 };
861 table_move.update(cx, |table, cx| {
862 table.set_pivot_sidebar_width(drag.start_width + delta);
863 cx.notify();
864 });
865 })
866 .on_mouse_up(MouseButton::Left, move |_event, _window, cx| {
867 table_up.update(cx, |table, cx| {
868 table.pivot_sidebar_drag = None;
869 cx.notify();
870 });
871 })
872 .on_mouse_up_out(MouseButton::Left, move |_event, _window, cx| {
873 table_up_out.update(cx, |table, cx| {
874 table.pivot_sidebar_drag = None;
875 cx.notify();
876 });
877 });
878 }
879
880 root
881 }
882}
883
884impl SqllyDataTable {
885 fn render_grid_view(&mut self, cx: &mut Context<Self>) -> Div {
888 let state_canvas = self.state.clone();
889 let state_status = self.state.clone();
890 let state_mouse = self.state.clone();
891 let state_move = self.state.clone();
892 let state_up = self.state.clone();
893 let state_scroll = self.state.clone();
894 let state_key = self.state.clone();
895 let state_right = self.state.clone();
896 let bg = self.state.read(cx).theme.bg;
897 let focus_handle = self.state.read(cx).focus_handle.clone();
898 let focus_left = focus_handle.clone();
899 let focus_right = focus_handle.clone();
900 let debug_bar = self.state.read(cx).debug_bar_enabled;
901 let status_h = self.state.read(cx).status_bar_height;
902
903 if let Some((action, col)) = self.state.read(cx).pending_action {
906 self.state.update(cx, |s, cx| {
907 s.execute_action(action, col, cx);
908 s.pending_action = None;
909 });
910 }
911
912 if let Some(pending) = self
914 .state
915 .read(cx)
916 .pending_custom_context_menu_action
917 .clone()
918 {
919 self.state.update(cx, |s, cx| {
920 s.pending_custom_context_menu_action = None;
921 s.execute_custom_context_menu_action(pending, cx);
922 });
923 }
924
925 if self.state.read(cx).is_dragging && !self.state.read(cx).edge_scroll_active {
933 self.state.update(cx, |s, _cx| s.edge_scroll_active = true);
934 let state_edge = self.state.clone();
935 cx.spawn(async move |_weak, cx| {
936 loop {
937 gpui::Timer::after(std::time::Duration::from_millis(EDGE_SCROLL_TICK_MS)).await;
938 let res = cx.update(|cx| state_edge.update(cx, |s, _cx| s.apply_edge_scroll()));
939 if let Ok(true) = res {
940 let _ = state_edge.update(cx, |_s, cx| cx.notify());
941 }
942 let dragging_res = cx.update(|cx| state_edge.read(cx).is_dragging);
943 if !matches!(dragging_res, Ok(true)) {
944 break;
945 }
946 }
947 let _ =
948 cx.update(|cx| state_edge.update(cx, |s, _cx| s.edge_scroll_active = false));
949 })
950 .detach();
951 }
952
953 div()
954 .flex()
955 .flex_col()
956 .size_full()
957 .relative()
958 .track_focus(&focus_handle)
959 .bg(bg)
960 .child(
961 canvas(
962 move |bounds, window, cx| -> PaintData {
963 let viewport = window.viewport_size();
964 state_canvas.update(cx, |s, cx| {
965 let mut dirty = false;
966 if s.bounds != bounds {
967 s.bounds = bounds;
968 s.clamp_scroll_to_bounds();
969 dirty = true;
970 }
971 if s.window_viewport != viewport {
972 s.window_viewport = viewport;
973 }
974 if dirty {
975 cx.notify();
976 }
977 });
978 let s = state_canvas.read(cx);
979 let mut data = PaintData::from_state(s);
980 data.focused = s.focus_handle.is_focused(window);
981 data
982 },
983 move |bounds, data, window, cx| {
984 paint_grid(&data, window, cx, bounds);
985 },
986 )
987 .flex_1(),
988 )
989 .children(debug_bar.then(|| {
990 canvas(
991 move |_bounds, _window, cx| -> StatusBarData {
992 let s = state_status.read(cx);
993 StatusBarData::from_state(s)
994 },
995 move |bounds, data, window, cx| {
996 paint_status_bar(&data, window, cx, bounds);
997 },
998 )
999 .h(px(status_h))
1000 }))
1001 .children(render_context_menu_overlay(&self.state, cx))
1002 .children(render_filter_panel_overlay(&self.state, cx))
1003 .children(render_busy_overlay(&self.state, cx))
1004 .on_mouse_down(
1005 MouseButton::Left,
1006 move |event: &MouseDownEvent, window, cx| {
1007 window.focus(&focus_left);
1008 state_mouse.update(cx, |s, cx| {
1009 if s.busy.is_some() {
1012 return;
1013 }
1014 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
1020 if s.context_menu.is_some() || s.filter_panel.is_some() {
1021 s.context_menu = None;
1022 s.filter_panel = None;
1023 }
1024 s.handle_mouse_down_with_modifiers(
1025 rel,
1026 event.modifiers.shift,
1027 event.modifiers.platform,
1028 );
1029 cx.notify();
1030 });
1031 },
1032 )
1033 .on_mouse_down(
1034 MouseButton::Right,
1035 move |event: &MouseDownEvent, window, cx| {
1036 window.focus(&focus_right);
1037 state_right.update(cx, |s, cx| {
1038 if s.busy.is_some() {
1039 return;
1040 }
1041 let pos = state_inner::to_grid_relative(event.position, s.bounds.origin);
1042 let hit = s.hit_test(pos);
1043
1044 if s.context_menu_provider.is_none() {
1046 match hit {
1047 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
1048 s.open_context_menu(col, pos);
1049 }
1050 _ => {
1051 s.context_menu = None;
1052 s.filter_panel = None;
1053 }
1054 }
1055 cx.notify();
1056 return;
1057 }
1058
1059 let Some(target) = s.context_menu_target_from_hit(hit) else {
1061 s.context_menu = None;
1062 s.filter_panel = None;
1063 cx.notify();
1064 return;
1065 };
1066
1067 let effective = s.effective_selection_for_context_target(&target);
1068 if effective != s.selection {
1069 s.selection = effective.clone();
1070 }
1071
1072 let request = s.build_context_menu_request(target, &effective);
1073 let col = request.target.column_index().unwrap_or(0);
1074
1075 let Some(provider) = s.context_menu_provider.clone() else {
1076 return;
1077 };
1078 let public_items = provider.menu_items(&request);
1079 let items = GridState::convert_context_menu_items(public_items);
1080
1081 if items.is_empty() {
1082 s.context_menu = None;
1083 } else {
1084 s.context_menu =
1085 Some(menu::ContextMenu::custom(col, pos, items, request));
1086 }
1087 s.filter_panel = None;
1088 cx.notify();
1089 });
1090 },
1091 )
1092 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
1093 state_move.update(cx, |s, cx| {
1094 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
1095 s.handle_mouse_move(rel, event.pressed_button);
1096 cx.notify();
1097 });
1098 })
1099 .on_mouse_up(
1100 MouseButton::Left,
1101 move |_event: &MouseUpEvent, _window, cx| {
1102 state_up.update(cx, |s, cx| {
1103 s.handle_mouse_up();
1104 cx.notify();
1105 });
1106 },
1107 )
1108 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
1109 state_scroll.update(cx, |s, cx| {
1110 let line_h = px(s.row_height);
1111 let delta = event.delta.pixel_delta(line_h);
1112 let scroll = s.scroll_handle.offset();
1113 let (mx, my) = s.max_scroll();
1114 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
1115 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
1116 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
1117 if s.drag_start.is_some() {
1118 s.handle_scroll_drag();
1119 }
1120 cx.notify();
1121 });
1122 })
1123 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
1124 let ks = &event.keystroke;
1125 if ks.modifiers.platform && ks.key == "q" {
1126 cx.quit();
1127 return;
1128 }
1129 state_key.update(cx, |s, cx| {
1130 let kb = &s.config.key_bindings;
1131 if kb.select_all.matches(ks) {
1132 s.select_all();
1133 } else if kb.copy.matches(ks) {
1134 s.copy_selection(false, cx);
1135 } else if kb.copy_with_headers.matches(ks) {
1136 s.copy_selection(true, cx);
1137 } else if kb.page_up.matches(ks) {
1138 s.page_up();
1139 } else if kb.page_down.matches(ks) {
1140 s.page_down();
1141 } else {
1142 s.handle_key(ks);
1143 }
1144 cx.notify();
1145 });
1146 })
1147 }
1148}
1149
1150fn render_context_menu_overlay(
1162 state: &Entity<GridState>,
1163 cx: &mut Context<SqllyDataTable>,
1164) -> Option<impl IntoElement> {
1165 let s = state.read(cx);
1166 let menu = s.context_menu.clone()?;
1167 let theme = s.theme.clone();
1168 let cw = s.char_width;
1169 let grid_ox = f32::from(s.bounds.origin.x);
1170 let grid_oy = f32::from(s.bounds.origin.y);
1171 let viewport = s.window_viewport;
1172 let vw = f32::from(viewport.width);
1173 let vh = f32::from(viewport.height);
1174
1175 let resolved = menu.resolved_position(grid_ox, grid_oy, vw, vh, cw);
1176 let abs_x = grid_ox + f32::from(resolved.x);
1177 let abs_y = grid_oy + f32::from(resolved.y);
1178 let menu_w = menu.width_for(cw);
1179
1180 let mut rows: Vec<gpui::AnyElement> = Vec::with_capacity(menu.items.len());
1183 let mut selectable_idx = 0usize;
1184 for item in &menu.items {
1185 match item {
1186 MenuItem::Separator => {
1187 rows.push(
1188 div()
1189 .h(px(menu::MENU_ITEM_HEIGHT))
1190 .flex()
1191 .items_center()
1192 .child(div().mx(px(4.0)).h(px(1.0)).w_full().bg(theme.grid_line))
1193 .into_any_element(),
1194 );
1195 }
1196 MenuItem::Action(_) | MenuItem::Custom { .. } => {
1197 let this_idx = selectable_idx;
1198 selectable_idx += 1;
1199 let label = item.label().unwrap_or("").to_owned();
1200 let hovered = menu.hovered == Some(this_idx);
1201
1202 let action = match item {
1206 MenuItem::Action(a) => MenuDispatch::Builtin(*a, menu.col),
1207 MenuItem::Custom { id, .. } => {
1208 MenuDispatch::Custom(id.clone(), menu.request.clone())
1209 }
1210 MenuItem::Separator => unreachable!(),
1211 };
1212
1213 let state_click = state.clone();
1214 let state_hover = state.clone();
1215 let mut row = div()
1216 .h(px(menu::MENU_ITEM_HEIGHT))
1217 .px(px(menu::MENU_PADDING_X))
1218 .flex()
1219 .items_center()
1220 .text_color(theme.menu_fg)
1221 .text_size(px(menu::MENU_FONT_SIZE))
1222 .child(label)
1223 .on_mouse_move(move |_e: &MouseMoveEvent, _window, cx| {
1224 state_hover.update(cx, |s, cx| {
1225 if let Some(m) = s.context_menu.as_mut() {
1226 if m.hovered != Some(this_idx) {
1227 m.hovered = Some(this_idx);
1228 cx.notify();
1229 }
1230 }
1231 });
1232 })
1233 .on_mouse_down(
1234 MouseButton::Left,
1235 move |_e: &MouseDownEvent, _window, cx| {
1236 state_click.update(cx, |s, cx| {
1237 match &action {
1238 MenuDispatch::Builtin(a, col) => {
1239 s.pending_action = Some((*a, *col));
1240 }
1241 MenuDispatch::Custom(id, request) => {
1242 if let Some(request) = request {
1243 s.pending_custom_context_menu_action =
1244 Some(PendingCustomContextMenuAction {
1245 id: id.clone(),
1246 request: request.clone(),
1247 });
1248 }
1249 }
1250 }
1251 s.context_menu = None;
1252 cx.notify();
1253 });
1254 },
1255 );
1256 if hovered {
1257 row = row.bg(theme.menu_hover_bg);
1258 }
1259 rows.push(row.into_any_element());
1260 }
1261 }
1262 }
1263
1264 let menu_body = div()
1265 .flex()
1266 .flex_col()
1267 .w(px(menu_w))
1268 .py(px(menu::MENU_INNER_PAD))
1269 .bg(theme.menu_bg)
1270 .border_1()
1271 .border_color(theme.grid_line)
1272 .children(rows);
1273
1274 let state_backdrop = state.clone();
1277 let overlay = deferred(anchored().position(point(px(abs_x), px(abs_y))).child(
1278 div().occlude().child(menu_body).on_mouse_down_out(
1279 move |_e: &MouseDownEvent, _window, cx| {
1280 state_backdrop.update(cx, |s, cx| {
1281 if s.context_menu.is_some() {
1282 s.context_menu = None;
1283 s.filter_panel = None;
1284 cx.notify();
1285 }
1286 });
1287 },
1288 ),
1289 ))
1290 .with_priority(CONTEXT_MENU_PRIORITY);
1291
1292 Some(overlay)
1293}
1294
1295const FILTER_PANEL_WIDTH: f32 = 300.0;
1297const FILTER_PANEL_MAX_ROWS: usize = 200;
1299
1300#[allow(clippy::too_many_lines)]
1305fn render_filter_panel_overlay(
1306 state: &Entity<GridState>,
1307 cx: &mut Context<SqllyDataTable>,
1308) -> Option<impl IntoElement> {
1309 let s = state.read(cx);
1310 let panel = s.filter_panel.clone()?;
1311 let theme = s.theme.clone();
1312 let col = panel.col;
1313 let current_sort = s.sort;
1314 let filter_active = s.filters.get(col).is_some_and(|f| f.is_active());
1315 let grid_ox = f32::from(s.bounds.origin.x);
1316 let grid_oy = f32::from(s.bounds.origin.y);
1317
1318 let abs_x = grid_ox + f32::from(panel.anchor.x);
1323 let abs_y = grid_oy + f32::from(panel.anchor.y);
1324
1325 let c_bg = theme.menu_bg;
1327 let c_line = theme.grid_line;
1328 let c_fg = theme.menu_fg;
1329 let c_accent = theme.sort_indicator;
1330 let c_hover = theme.menu_hover_bg;
1331 let c_muted = theme.muted_text;
1332
1333 let checkbox = move |checked: bool| {
1334 let mut b = div()
1335 .w(px(14.0))
1336 .h(px(14.0))
1337 .border_1()
1338 .border_color(c_line)
1339 .bg(c_bg)
1340 .flex()
1341 .items_center()
1342 .justify_center();
1343 if checked {
1344 b = b.child(div().w(px(8.0)).h(px(8.0)).bg(c_accent));
1345 }
1346 b
1347 };
1348
1349 let (asc_active, desc_active) = match current_sort {
1351 Some((c, SortDirection::Ascending)) if c == col => (true, false),
1352 Some((c, SortDirection::Descending)) if c == col => (false, true),
1353 _ => (false, false),
1354 };
1355 let st_asc = state.clone();
1356 let st_desc = state.clone();
1357 let sort_row = div()
1358 .flex()
1359 .gap(px(6.0))
1360 .child(
1361 div()
1362 .flex_1()
1363 .h(px(26.0))
1364 .flex()
1365 .items_center()
1366 .justify_center()
1367 .border_1()
1368 .border_color(c_line)
1369 .bg(if asc_active { c_accent } else { c_hover })
1370 .cursor_pointer()
1371 .child("Ascending")
1372 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1373 st_asc.update(cx, |s, cx| {
1374 s.set_panel_sort(SortDirection::Ascending);
1375 cx.notify();
1376 });
1377 }),
1378 )
1379 .child(
1380 div()
1381 .flex_1()
1382 .h(px(26.0))
1383 .flex()
1384 .items_center()
1385 .justify_center()
1386 .border_1()
1387 .border_color(c_line)
1388 .bg(if desc_active { c_accent } else { c_hover })
1389 .cursor_pointer()
1390 .child("Descending")
1391 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1392 st_desc.update(cx, |s, cx| {
1393 s.set_panel_sort(SortDirection::Descending);
1394 cx.notify();
1395 });
1396 }),
1397 );
1398
1399 let st_op_toggle = state.clone();
1401 let op_button = div()
1402 .h(px(26.0))
1403 .px(px(8.0))
1404 .flex()
1405 .items_center()
1406 .border_1()
1407 .border_color(c_line)
1408 .bg(c_bg)
1409 .cursor_pointer()
1410 .child(panel.current_op_label())
1411 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1412 st_op_toggle.update(cx, |s, cx| {
1413 s.toggle_filter_op_menu();
1414 cx.notify();
1415 });
1416 });
1417
1418 let op_menu = panel.op_menu_open.then(|| {
1419 let mut items: Vec<gpui::AnyElement> = Vec::new();
1420 for (i, label) in panel.op_labels().iter().enumerate() {
1421 let selected = i == panel.op_index;
1422 let st_pick = state.clone();
1423 items.push(
1424 div()
1425 .h(px(24.0))
1426 .px(px(8.0))
1427 .flex()
1428 .items_center()
1429 .bg(if selected { c_accent } else { c_bg })
1430 .cursor_pointer()
1431 .child(*label)
1432 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1433 st_pick.update(cx, |s, cx| {
1434 s.set_filter_operator(i);
1435 cx.notify();
1436 });
1437 })
1438 .into_any_element(),
1439 );
1440 }
1441 div()
1442 .flex()
1443 .flex_col()
1444 .border_1()
1445 .border_color(c_line)
1446 .bg(c_bg)
1447 .children(items)
1448 });
1449
1450 let operand_field = |value: &str, focused: bool, placeholder: &str, input: FilterInput| {
1452 let st_focus = state.clone();
1453 let (text, is_placeholder) = if value.is_empty() {
1454 (placeholder.to_owned(), true)
1455 } else {
1456 (value.to_owned(), false)
1457 };
1458 div()
1459 .h(px(26.0))
1460 .px(px(6.0))
1461 .flex()
1462 .items_center()
1463 .gap(px(2.0))
1464 .border_1()
1465 .border_color(if focused { c_accent } else { c_line })
1466 .bg(c_bg)
1467 .cursor_pointer()
1468 .child(
1469 div()
1470 .text_color(if is_placeholder { c_muted } else { c_fg })
1471 .child(text),
1472 )
1473 .children(focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
1474 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1475 st_focus.update(cx, |s, cx| {
1476 s.set_filter_focus(input);
1477 cx.notify();
1478 });
1479 })
1480 };
1481
1482 let operand_placeholder = if panel.kind == crate::data::ColumnKind::Date {
1483 "YYYY-MM-DD"
1484 } else if crate::filter::uses_number_ops(panel.kind) {
1485 "value"
1486 } else if panel.op_index == 7 {
1487 "regex"
1489 } else {
1490 "value"
1491 };
1492 let operands = panel.needs_operand().then(|| {
1493 let mut row = div().flex().flex_col().gap(px(4.0)).child(operand_field(
1494 &panel.operand_a.value,
1495 panel.focus == FilterInput::OperandA,
1496 operand_placeholder,
1497 FilterInput::OperandA,
1498 ));
1499 if panel.needs_second_operand() {
1500 row = row
1501 .child(div().text_color(c_muted).text_size(px(11.0)).child("and"))
1502 .child(operand_field(
1503 &panel.operand_b.value,
1504 panel.focus == FilterInput::OperandB,
1505 operand_placeholder,
1506 FilterInput::OperandB,
1507 ));
1508 }
1509 row
1510 });
1511
1512 let st_search = state.clone();
1514 let search_focused = panel.focus == FilterInput::Search;
1515 let (search_text, search_is_ph) = if panel.search.value.is_empty() {
1516 ("Search".to_owned(), true)
1517 } else {
1518 (panel.search.value.clone(), false)
1519 };
1520 let search_box = div()
1521 .h(px(26.0))
1522 .px(px(6.0))
1523 .flex()
1524 .items_center()
1525 .gap(px(2.0))
1526 .border_1()
1527 .border_color(if search_focused { c_accent } else { c_line })
1528 .bg(c_bg)
1529 .cursor_pointer()
1530 .child(
1531 div()
1532 .text_color(if search_is_ph { c_muted } else { c_fg })
1533 .child(search_text),
1534 )
1535 .children(search_focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
1536 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1537 st_search.update(cx, |s, cx| {
1538 s.set_filter_focus(FilterInput::Search);
1539 cx.notify();
1540 });
1541 });
1542
1543 let st_all = state.clone();
1545 let select_all_row = div()
1546 .h(px(24.0))
1547 .flex()
1548 .items_center()
1549 .gap(px(6.0))
1550 .cursor_pointer()
1551 .child(checkbox(panel.all_checked()))
1552 .child("(Select All)")
1553 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1554 st_all.update(cx, |s, cx| {
1555 s.toggle_filter_select_all();
1556 cx.notify();
1557 });
1558 });
1559
1560 let visible = panel.visible_indices();
1561 let mut value_rows: Vec<gpui::AnyElement> = Vec::new();
1562 for &idx in visible.iter().take(FILTER_PANEL_MAX_ROWS) {
1563 let row = &panel.distinct[idx];
1564 let st_val = state.clone();
1565 value_rows.push(
1566 div()
1567 .h(px(22.0))
1568 .flex()
1569 .items_center()
1570 .gap(px(6.0))
1571 .cursor_pointer()
1572 .child(checkbox(row.checked))
1573 .child(div().text_color(c_fg).child(row.label.clone()))
1574 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1575 st_val.update(cx, |s, cx| {
1576 s.toggle_filter_value(idx);
1577 cx.notify();
1578 });
1579 })
1580 .into_any_element(),
1581 );
1582 }
1583 let truncated = visible.len() > FILTER_PANEL_MAX_ROWS;
1584 let value_list = div()
1585 .id("filter-value-list")
1586 .flex()
1587 .flex_col()
1588 .max_h(px(180.0))
1589 .overflow_y_scroll()
1590 .children(value_rows)
1591 .children(truncated.then(|| {
1592 div()
1593 .text_color(c_muted)
1594 .text_size(px(11.0))
1595 .child("Refine search to see more…")
1596 }));
1597
1598 let st_clear = state.clone();
1600 let st_close = state.clone();
1601 let clear_bg = if filter_active { c_hover } else { c_bg };
1602 let clear_fg = if filter_active { c_fg } else { c_muted };
1603 let clear_border = if filter_active { c_line } else { c_muted };
1604 let buttons_row = div()
1605 .flex()
1606 .gap(px(6.0))
1607 .child(
1608 div()
1609 .flex_1()
1610 .h(px(28.0))
1611 .flex()
1612 .items_center()
1613 .justify_center()
1614 .border_1()
1615 .border_color(clear_border)
1616 .bg(clear_bg)
1617 .text_color(clear_fg)
1618 .cursor_pointer()
1619 .child("Clear Filter")
1620 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1621 if !filter_active {
1622 return;
1623 }
1624 st_clear.update(cx, |s, cx| {
1625 s.clear_filter_panel();
1626 cx.notify();
1627 });
1628 }),
1629 )
1630 .child(
1631 div()
1632 .flex_1()
1633 .h(px(28.0))
1634 .flex()
1635 .items_center()
1636 .justify_center()
1637 .border_1()
1638 .border_color(c_line)
1639 .bg(c_hover)
1640 .cursor_pointer()
1641 .child("Close")
1642 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1643 st_close.update(cx, |s, cx| {
1644 s.filter_panel = None;
1645 cx.notify();
1646 });
1647 }),
1648 );
1649
1650 let panel_body = div()
1651 .flex()
1652 .flex_col()
1653 .w(px(FILTER_PANEL_WIDTH))
1654 .p(px(10.0))
1655 .gap(px(8.0))
1656 .bg(c_bg)
1657 .border_1()
1658 .border_color(c_line)
1659 .text_color(c_fg)
1660 .text_size(px(13.0))
1661 .child(div().text_color(c_muted).text_size(px(11.0)).child("Sort"))
1662 .child(sort_row)
1663 .child(
1664 div()
1665 .text_color(c_muted)
1666 .text_size(px(11.0))
1667 .child("Filter"),
1668 )
1669 .child(op_button)
1670 .children(op_menu)
1671 .children(operands)
1672 .child(search_box)
1673 .child(select_all_row)
1674 .child(value_list)
1675 .child(buttons_row);
1676
1677 let st_backdrop = state.clone();
1678 let overlay = deferred(
1679 anchored()
1680 .anchor(Corner::BottomLeft)
1681 .position(point(px(abs_x), px(abs_y)))
1682 .child(div().occlude().child(panel_body).on_mouse_down_out(
1683 move |_e: &MouseDownEvent, _window, cx| {
1684 st_backdrop.update(cx, |s, cx| {
1685 if s.filter_panel.is_some() {
1686 s.filter_panel = None;
1687 cx.notify();
1688 }
1689 });
1690 },
1691 )),
1692 )
1693 .with_priority(CONTEXT_MENU_PRIORITY);
1694
1695 Some(overlay)
1696}
1697
1698fn render_busy_overlay(
1704 state: &Entity<GridState>,
1705 cx: &mut Context<SqllyDataTable>,
1706) -> Option<impl IntoElement> {
1707 let s = state.read(cx);
1708 let busy = s.busy.clone()?;
1709 let theme = s.theme.clone();
1710 let track = theme.grid_line;
1711 let accent = theme.sort_indicator;
1712
1713 let bar: gpui::AnyElement = if let Some(p) = busy.progress {
1714 let p = p.clamp(0.0, 1.0);
1715 div()
1716 .h_full()
1717 .w(relative(p))
1718 .rounded(px(3.0))
1719 .bg(accent)
1720 .into_any_element()
1721 } else {
1722 div()
1723 .h_full()
1724 .w(relative(0.3))
1725 .rounded(px(3.0))
1726 .bg(accent)
1727 .with_animation(
1728 "busy-indeterminate",
1729 Animation::new(std::time::Duration::from_millis(900))
1730 .repeat()
1731 .with_easing(pulsating_between(0.15, 0.85)),
1732 |el, delta| el.w(relative(delta)),
1733 )
1734 .into_any_element()
1735 };
1736
1737 let card = div()
1738 .flex()
1739 .flex_col()
1740 .gap(px(10.0))
1741 .p(px(16.0))
1742 .min_w(px(220.0))
1743 .rounded(px(8.0))
1744 .bg(theme.menu_bg)
1745 .border_1()
1746 .border_color(theme.grid_line)
1747 .child(
1748 div()
1749 .text_color(theme.menu_fg)
1750 .text_size(px(14.0))
1751 .child(busy.label.clone()),
1752 )
1753 .child(
1754 div()
1755 .w_full()
1756 .h(px(6.0))
1757 .rounded(px(3.0))
1758 .bg(track)
1759 .child(bar),
1760 );
1761
1762 let overlay = div()
1763 .absolute()
1764 .top_0()
1765 .left_0()
1766 .size_full()
1767 .occlude()
1768 .flex()
1769 .items_center()
1770 .justify_center()
1771 .bg(theme.overlay_scrim)
1772 .child(card);
1773
1774 Some(overlay)
1775}
1776
1777enum MenuDispatch {
1780 Builtin(menu::MenuAction, usize),
1781 Custom(
1782 String,
1783 Option<crate::grid::context_menu::ContextMenuRequest>,
1784 ),
1785}