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.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 && self.appearance_subscription.is_none() {
608 let appearance = window.appearance();
609 self.state.update(cx, |s, _cx| {
610 s.theme = s.theme_family.for_appearance(appearance);
611 });
612 let state_appearance = self.state.clone();
613 self.appearance_subscription =
614 Some(window.observe_window_appearance(move |window, cx| {
615 let appearance = window.appearance();
616 state_appearance.update(cx, |s, cx| {
617 s.theme = s.theme_family.for_appearance(appearance);
618 cx.notify();
619 });
620 }));
621 }
622
623 if let Some(parts) = &self.pivot {
628 let grid_theme = self.state.read(cx).theme.clone();
629 let sidebar_width = self.pivot_sidebar_width;
630 parts.state.update(cx, |s, cx| {
631 let mut dirty = false;
632 if s.theme != grid_theme {
633 s.theme = grid_theme;
634 dirty = true;
635 }
636 if (s.sidebar_width - sidebar_width).abs() > 0.5 {
637 s.sidebar_width = sidebar_width;
638 dirty = true;
639 }
640 if dirty {
641 cx.notify();
642 }
643 });
644 }
645
646 if let Some(parts) = &self.pivot {
651 let drill = parts.state.update(cx, |s, _cx| s.take_pending_drill_down());
652 if let Some(filter_sets) = drill {
653 self.state.update(cx, |g, cx| {
654 if g.window.is_none() {
658 for filter in &mut g.filters {
659 *filter = ColumnFilter::default();
660 }
661 for (field, values) in filter_sets {
662 if let Some(slot) = g.filters.get_mut(field) {
663 *slot = ColumnFilter {
664 predicate: FilterPredicate::None,
665 values: Some(values),
666 };
667 }
668 }
669 g.recompute();
670 }
671 cx.notify();
672 });
673 self.active_tab = GridTab::Grid;
674 let focus = self.state.read(cx).focus_handle.clone();
675 window.focus(&focus);
676 cx.notify();
677 }
678 }
679
680 let grid_view = self.render_grid_view(cx);
681
682 let Some(parts) = &self.pivot else {
683 return div().size_full().child(grid_view);
684 };
685
686 let theme = self.state.read(cx).theme.clone();
687 let tab = |label: String,
688 this_tab: GridTab,
689 active: bool,
690 locked: bool,
691 cx: &mut Context<Self>| {
692 div()
693 .px(px(14.0))
694 .py(px(5.0))
695 .text_size(px(13.0))
696 .when(!locked, |tab| tab.cursor_pointer())
697 .when(locked, |tab| tab.opacity(0.55))
698 .bg(if active { theme.bg } else { theme.header_bg })
699 .text_color(if active {
700 theme.header_fg
701 } else {
702 theme.muted_text
703 })
704 .border_b_2()
705 .border_color(if active {
706 theme.sort_indicator
707 } else {
708 theme.header_bg
709 })
710 .child(label)
711 .when(!locked, |tab| {
712 tab.on_mouse_down(
713 MouseButton::Left,
714 cx.listener(move |this, _e: &MouseDownEvent, window, cx| {
715 if this.active_tab == this_tab {
716 return;
717 }
718 this.set_active_tab(this_tab, cx);
719 match this_tab {
721 GridTab::Grid => {
722 let focus = this.state.read(cx).focus_handle.clone();
723 window.focus(&focus);
724 }
725 GridTab::Pivot => {
726 if let Some(p) = &this.pivot {
727 let focus = p.state.read(cx).focus_handle.clone();
728 window.focus(&focus);
729 }
730 }
731 }
732 cx.notify();
733 }),
734 )
735 })
736 };
737
738 let pivot_label = self
739 .pivot_status
740 .as_deref()
741 .map_or_else(|| "Pivot".to_string(), |status| format!("Pivot {status}"));
742
743 let tab_bar = div()
744 .flex()
745 .flex_row()
746 .flex_none()
747 .bg(theme.header_bg)
748 .border_b_1()
749 .border_color(theme.grid_line)
750 .child(tab(
751 "Grid".to_string(),
752 GridTab::Grid,
753 self.active_tab == GridTab::Grid,
754 false,
755 cx,
756 ))
757 .child(tab(
758 pivot_label,
759 GridTab::Pivot,
760 self.active_tab == GridTab::Pivot,
761 self.pivot_locked,
762 cx,
763 ));
764
765 let content: gpui::AnyElement = match self.active_tab {
766 GridTab::Grid => grid_view.into_any_element(),
767 GridTab::Pivot => {
768 let position = self.pivot_sidebar_position;
769 let collapsed = self.pivot_sidebar_collapsed;
770 let panel = div()
771 .w(px(self.pivot_sidebar_width))
772 .h_full()
773 .flex_none()
774 .child(parts.sidebar.clone());
775 let pivot_grid = div().flex_1().min_w_0().child(parts.grid.clone());
776 let direction = match position {
777 PivotSidebarPosition::Left => CollapseDirection::Left,
778 PivotSidebarPosition::Right => CollapseDirection::Right,
779 };
780 let divider_theme = PaneDividerTheme {
781 background: theme.header_bg.into(),
782 background_hover: theme.menu_hover_bg.into(),
783 background_collapsed: theme.menu_bg.into(),
784 foreground: theme.muted_text.into(),
785 foreground_hover: theme.header_fg.into(),
786 border: theme.grid_line.into(),
787 };
788 let table_toggle = cx.entity().clone();
789 let table_drag = cx.entity().clone();
790 let divider = PaneDivider::vertical("pivot-sidebar-divider", direction)
791 .label("Pivot")
792 .collapsed(collapsed)
793 .theme(divider_theme)
794 .on_toggle(move |collapsed, _window, cx| {
795 table_toggle.update(cx, |table, cx| {
796 table.set_pivot_sidebar_collapsed(collapsed);
797 cx.notify();
798 });
799 })
800 .on_drag_start(move |start_x, _window, cx| {
801 table_drag.update(cx, |table, cx| {
802 table.pivot_sidebar_drag = Some(PivotSidebarDrag {
803 start_x,
804 start_width: table.pivot_sidebar_width,
805 });
806 cx.notify();
807 });
808 });
809
810 let mut pivot_view = div().flex().flex_row().size_full();
811 pivot_view = match position {
812 PivotSidebarPosition::Left => pivot_view
813 .children((!collapsed).then_some(panel))
814 .child(divider)
815 .child(pivot_grid),
816 PivotSidebarPosition::Right => pivot_view
817 .child(pivot_grid)
818 .child(divider)
819 .children((!collapsed).then_some(panel)),
820 };
821
822 pivot_view.into_any_element()
823 }
824 };
825
826 let mut root = div()
827 .flex()
828 .flex_col()
829 .size_full()
830 .child(tab_bar)
831 .child(div().flex_1().min_h_0().child(content));
832
833 if let Some(drag) = self.pivot_sidebar_drag {
834 let position = self.pivot_sidebar_position;
835 let table_move = cx.entity().clone();
836 let table_up = cx.entity().clone();
837 let table_up_out = cx.entity().clone();
838 root = root
839 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
840 let current_x: f32 = event.position.x.into();
841 let delta = match position {
842 PivotSidebarPosition::Left => current_x - drag.start_x,
843 PivotSidebarPosition::Right => drag.start_x - current_x,
844 };
845 table_move.update(cx, |table, cx| {
846 table.set_pivot_sidebar_width(drag.start_width + delta);
847 cx.notify();
848 });
849 })
850 .on_mouse_up(MouseButton::Left, move |_event, _window, cx| {
851 table_up.update(cx, |table, cx| {
852 table.pivot_sidebar_drag = None;
853 cx.notify();
854 });
855 })
856 .on_mouse_up_out(MouseButton::Left, move |_event, _window, cx| {
857 table_up_out.update(cx, |table, cx| {
858 table.pivot_sidebar_drag = None;
859 cx.notify();
860 });
861 });
862 }
863
864 root
865 }
866}
867
868impl SqllyDataTable {
869 fn render_grid_view(&mut self, cx: &mut Context<Self>) -> Div {
872 let state_canvas = self.state.clone();
873 let state_status = self.state.clone();
874 let state_mouse = self.state.clone();
875 let state_move = self.state.clone();
876 let state_up = self.state.clone();
877 let state_scroll = self.state.clone();
878 let state_key = self.state.clone();
879 let state_right = self.state.clone();
880 let bg = self.state.read(cx).theme.bg;
881 let focus_handle = self.state.read(cx).focus_handle.clone();
882 let focus_left = focus_handle.clone();
883 let focus_right = focus_handle.clone();
884 let debug_bar = self.state.read(cx).debug_bar_enabled;
885 let status_h = self.state.read(cx).status_bar_height;
886
887 if let Some((action, col)) = self.state.read(cx).pending_action {
890 self.state.update(cx, |s, cx| {
891 s.execute_action(action, col, cx);
892 s.pending_action = None;
893 });
894 }
895
896 if let Some(pending) = self
898 .state
899 .read(cx)
900 .pending_custom_context_menu_action
901 .clone()
902 {
903 self.state.update(cx, |s, cx| {
904 s.pending_custom_context_menu_action = None;
905 s.execute_custom_context_menu_action(pending, cx);
906 });
907 }
908
909 if self.state.read(cx).is_dragging && !self.state.read(cx).edge_scroll_active {
917 self.state.update(cx, |s, _cx| s.edge_scroll_active = true);
918 let state_edge = self.state.clone();
919 cx.spawn(async move |_weak, cx| {
920 loop {
921 gpui::Timer::after(std::time::Duration::from_millis(EDGE_SCROLL_TICK_MS)).await;
922 let res = cx.update(|cx| state_edge.update(cx, |s, _cx| s.apply_edge_scroll()));
923 if let Ok(true) = res {
924 let _ = state_edge.update(cx, |_s, cx| cx.notify());
925 }
926 let dragging_res = cx.update(|cx| state_edge.read(cx).is_dragging);
927 if !matches!(dragging_res, Ok(true)) {
928 break;
929 }
930 }
931 let _ =
932 cx.update(|cx| state_edge.update(cx, |s, _cx| s.edge_scroll_active = false));
933 })
934 .detach();
935 }
936
937 div()
938 .flex()
939 .flex_col()
940 .size_full()
941 .relative()
942 .track_focus(&focus_handle)
943 .bg(bg)
944 .child(
945 canvas(
946 move |bounds, window, cx| -> PaintData {
947 let viewport = window.viewport_size();
948 state_canvas.update(cx, |s, cx| {
949 let mut dirty = false;
950 if s.bounds != bounds {
951 s.bounds = bounds;
952 s.clamp_scroll_to_bounds();
953 dirty = true;
954 }
955 if s.window_viewport != viewport {
956 s.window_viewport = viewport;
957 }
958 if dirty {
959 cx.notify();
960 }
961 });
962 let s = state_canvas.read(cx);
963 PaintData::from_state(s)
964 },
965 move |bounds, data, window, cx| {
966 paint_grid(&data, window, cx, bounds);
967 },
968 )
969 .flex_1(),
970 )
971 .children(debug_bar.then(|| {
972 canvas(
973 move |_bounds, _window, cx| -> StatusBarData {
974 let s = state_status.read(cx);
975 StatusBarData::from_state(s)
976 },
977 move |bounds, data, window, cx| {
978 paint_status_bar(&data, window, cx, bounds);
979 },
980 )
981 .h(px(status_h))
982 }))
983 .children(render_context_menu_overlay(&self.state, cx))
984 .children(render_filter_panel_overlay(&self.state, cx))
985 .children(render_busy_overlay(&self.state, cx))
986 .on_mouse_down(
987 MouseButton::Left,
988 move |event: &MouseDownEvent, window, cx| {
989 window.focus(&focus_left);
990 state_mouse.update(cx, |s, cx| {
991 if s.busy.is_some() {
994 return;
995 }
996 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
1002 if s.context_menu.is_some() || s.filter_panel.is_some() {
1003 s.context_menu = None;
1004 s.filter_panel = None;
1005 }
1006 s.handle_mouse_down_with_modifiers(
1007 rel,
1008 event.modifiers.shift,
1009 event.modifiers.platform,
1010 );
1011 cx.notify();
1012 });
1013 },
1014 )
1015 .on_mouse_down(
1016 MouseButton::Right,
1017 move |event: &MouseDownEvent, window, cx| {
1018 window.focus(&focus_right);
1019 state_right.update(cx, |s, cx| {
1020 if s.busy.is_some() {
1021 return;
1022 }
1023 let pos = state_inner::to_grid_relative(event.position, s.bounds.origin);
1024 let hit = s.hit_test(pos);
1025
1026 if s.context_menu_provider.is_none() {
1028 match hit {
1029 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
1030 s.open_context_menu(col, pos);
1031 }
1032 _ => {
1033 s.context_menu = None;
1034 s.filter_panel = None;
1035 }
1036 }
1037 cx.notify();
1038 return;
1039 }
1040
1041 let Some(target) = s.context_menu_target_from_hit(hit) else {
1043 s.context_menu = None;
1044 s.filter_panel = None;
1045 cx.notify();
1046 return;
1047 };
1048
1049 let effective = s.effective_selection_for_context_target(&target);
1050 if effective != s.selection {
1051 s.selection = effective.clone();
1052 }
1053
1054 let request = s.build_context_menu_request(target, &effective);
1055 let col = request.target.column_index().unwrap_or(0);
1056
1057 let Some(provider) = s.context_menu_provider.clone() else {
1058 return;
1059 };
1060 let public_items = provider.menu_items(&request);
1061 let items = GridState::convert_context_menu_items(public_items);
1062
1063 if items.is_empty() {
1064 s.context_menu = None;
1065 } else {
1066 s.context_menu =
1067 Some(menu::ContextMenu::custom(col, pos, items, request));
1068 }
1069 s.filter_panel = None;
1070 cx.notify();
1071 });
1072 },
1073 )
1074 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
1075 state_move.update(cx, |s, cx| {
1076 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
1077 s.handle_mouse_move(rel, event.pressed_button);
1078 cx.notify();
1079 });
1080 })
1081 .on_mouse_up(
1082 MouseButton::Left,
1083 move |_event: &MouseUpEvent, _window, cx| {
1084 state_up.update(cx, |s, cx| {
1085 s.handle_mouse_up();
1086 cx.notify();
1087 });
1088 },
1089 )
1090 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
1091 state_scroll.update(cx, |s, cx| {
1092 let line_h = px(s.row_height);
1093 let delta = event.delta.pixel_delta(line_h);
1094 let scroll = s.scroll_handle.offset();
1095 let (mx, my) = s.max_scroll();
1096 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
1097 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
1098 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
1099 if s.drag_start.is_some() {
1100 s.handle_scroll_drag();
1101 }
1102 cx.notify();
1103 });
1104 })
1105 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
1106 let ks = &event.keystroke;
1107 if ks.modifiers.platform && ks.key == "q" {
1108 cx.quit();
1109 return;
1110 }
1111 state_key.update(cx, |s, cx| {
1112 let kb = &s.config.key_bindings;
1113 if kb.select_all.matches(ks) {
1114 s.select_all();
1115 } else if kb.copy.matches(ks) {
1116 s.copy_selection(false, cx);
1117 } else if kb.copy_with_headers.matches(ks) {
1118 s.copy_selection(true, cx);
1119 } else if kb.page_up.matches(ks) {
1120 s.page_up();
1121 } else if kb.page_down.matches(ks) {
1122 s.page_down();
1123 } else {
1124 s.handle_key(ks);
1125 }
1126 cx.notify();
1127 });
1128 })
1129 }
1130}
1131
1132fn render_context_menu_overlay(
1144 state: &Entity<GridState>,
1145 cx: &mut Context<SqllyDataTable>,
1146) -> Option<impl IntoElement> {
1147 let s = state.read(cx);
1148 let menu = s.context_menu.clone()?;
1149 let theme = s.theme.clone();
1150 let cw = s.char_width;
1151 let grid_ox = f32::from(s.bounds.origin.x);
1152 let grid_oy = f32::from(s.bounds.origin.y);
1153 let viewport = s.window_viewport;
1154 let vw = f32::from(viewport.width);
1155 let vh = f32::from(viewport.height);
1156
1157 let resolved = menu.resolved_position(grid_ox, grid_oy, vw, vh, cw);
1158 let abs_x = grid_ox + f32::from(resolved.x);
1159 let abs_y = grid_oy + f32::from(resolved.y);
1160 let menu_w = menu.width_for(cw);
1161
1162 let mut rows: Vec<gpui::AnyElement> = Vec::with_capacity(menu.items.len());
1165 let mut selectable_idx = 0usize;
1166 for item in &menu.items {
1167 match item {
1168 MenuItem::Separator => {
1169 rows.push(
1170 div()
1171 .h(px(menu::MENU_ITEM_HEIGHT))
1172 .flex()
1173 .items_center()
1174 .child(div().mx(px(4.0)).h(px(1.0)).w_full().bg(theme.grid_line))
1175 .into_any_element(),
1176 );
1177 }
1178 MenuItem::Action(_) | MenuItem::Custom { .. } => {
1179 let this_idx = selectable_idx;
1180 selectable_idx += 1;
1181 let label = item.label().unwrap_or("").to_owned();
1182 let hovered = menu.hovered == Some(this_idx);
1183
1184 let action = match item {
1188 MenuItem::Action(a) => MenuDispatch::Builtin(*a, menu.col),
1189 MenuItem::Custom { id, .. } => {
1190 MenuDispatch::Custom(id.clone(), menu.request.clone())
1191 }
1192 MenuItem::Separator => unreachable!(),
1193 };
1194
1195 let state_click = state.clone();
1196 let state_hover = state.clone();
1197 let mut row = div()
1198 .h(px(menu::MENU_ITEM_HEIGHT))
1199 .px(px(menu::MENU_PADDING_X))
1200 .flex()
1201 .items_center()
1202 .text_color(theme.menu_fg)
1203 .text_size(px(menu::MENU_FONT_SIZE))
1204 .child(label)
1205 .on_mouse_move(move |_e: &MouseMoveEvent, _window, cx| {
1206 state_hover.update(cx, |s, cx| {
1207 if let Some(m) = s.context_menu.as_mut() {
1208 if m.hovered != Some(this_idx) {
1209 m.hovered = Some(this_idx);
1210 cx.notify();
1211 }
1212 }
1213 });
1214 })
1215 .on_mouse_down(
1216 MouseButton::Left,
1217 move |_e: &MouseDownEvent, _window, cx| {
1218 state_click.update(cx, |s, cx| {
1219 match &action {
1220 MenuDispatch::Builtin(a, col) => {
1221 s.pending_action = Some((*a, *col));
1222 }
1223 MenuDispatch::Custom(id, request) => {
1224 if let Some(request) = request {
1225 s.pending_custom_context_menu_action =
1226 Some(PendingCustomContextMenuAction {
1227 id: id.clone(),
1228 request: request.clone(),
1229 });
1230 }
1231 }
1232 }
1233 s.context_menu = None;
1234 cx.notify();
1235 });
1236 },
1237 );
1238 if hovered {
1239 row = row.bg(theme.menu_hover_bg);
1240 }
1241 rows.push(row.into_any_element());
1242 }
1243 }
1244 }
1245
1246 let menu_body = div()
1247 .flex()
1248 .flex_col()
1249 .w(px(menu_w))
1250 .py(px(menu::MENU_INNER_PAD))
1251 .bg(theme.menu_bg)
1252 .border_1()
1253 .border_color(theme.grid_line)
1254 .children(rows);
1255
1256 let state_backdrop = state.clone();
1259 let overlay = deferred(anchored().position(point(px(abs_x), px(abs_y))).child(
1260 div().occlude().child(menu_body).on_mouse_down_out(
1261 move |_e: &MouseDownEvent, _window, cx| {
1262 state_backdrop.update(cx, |s, cx| {
1263 if s.context_menu.is_some() {
1264 s.context_menu = None;
1265 s.filter_panel = None;
1266 cx.notify();
1267 }
1268 });
1269 },
1270 ),
1271 ))
1272 .with_priority(CONTEXT_MENU_PRIORITY);
1273
1274 Some(overlay)
1275}
1276
1277const FILTER_PANEL_WIDTH: f32 = 300.0;
1279const FILTER_PANEL_MAX_ROWS: usize = 200;
1281
1282#[allow(clippy::too_many_lines)]
1287fn render_filter_panel_overlay(
1288 state: &Entity<GridState>,
1289 cx: &mut Context<SqllyDataTable>,
1290) -> Option<impl IntoElement> {
1291 let s = state.read(cx);
1292 let panel = s.filter_panel.clone()?;
1293 let theme = s.theme.clone();
1294 let col = panel.col;
1295 let current_sort = s.sort;
1296 let filter_active = s.filters.get(col).is_some_and(|f| f.is_active());
1297 let grid_ox = f32::from(s.bounds.origin.x);
1298 let grid_oy = f32::from(s.bounds.origin.y);
1299
1300 let abs_x = grid_ox + f32::from(panel.anchor.x);
1305 let abs_y = grid_oy + f32::from(panel.anchor.y);
1306
1307 let c_bg = theme.menu_bg;
1309 let c_line = theme.grid_line;
1310 let c_fg = theme.menu_fg;
1311 let c_accent = theme.sort_indicator;
1312 let c_hover = theme.menu_hover_bg;
1313 let c_muted = theme.muted_text;
1314
1315 let checkbox = move |checked: bool| {
1316 let mut b = div()
1317 .w(px(14.0))
1318 .h(px(14.0))
1319 .border_1()
1320 .border_color(c_line)
1321 .bg(c_bg)
1322 .flex()
1323 .items_center()
1324 .justify_center();
1325 if checked {
1326 b = b.child(div().w(px(8.0)).h(px(8.0)).bg(c_accent));
1327 }
1328 b
1329 };
1330
1331 let (asc_active, desc_active) = match current_sort {
1333 Some((c, SortDirection::Ascending)) if c == col => (true, false),
1334 Some((c, SortDirection::Descending)) if c == col => (false, true),
1335 _ => (false, false),
1336 };
1337 let st_asc = state.clone();
1338 let st_desc = state.clone();
1339 let sort_row = div()
1340 .flex()
1341 .gap(px(6.0))
1342 .child(
1343 div()
1344 .flex_1()
1345 .h(px(26.0))
1346 .flex()
1347 .items_center()
1348 .justify_center()
1349 .border_1()
1350 .border_color(c_line)
1351 .bg(if asc_active { c_accent } else { c_hover })
1352 .cursor_pointer()
1353 .child("Ascending")
1354 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1355 st_asc.update(cx, |s, cx| {
1356 s.set_panel_sort(SortDirection::Ascending);
1357 cx.notify();
1358 });
1359 }),
1360 )
1361 .child(
1362 div()
1363 .flex_1()
1364 .h(px(26.0))
1365 .flex()
1366 .items_center()
1367 .justify_center()
1368 .border_1()
1369 .border_color(c_line)
1370 .bg(if desc_active { c_accent } else { c_hover })
1371 .cursor_pointer()
1372 .child("Descending")
1373 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1374 st_desc.update(cx, |s, cx| {
1375 s.set_panel_sort(SortDirection::Descending);
1376 cx.notify();
1377 });
1378 }),
1379 );
1380
1381 let st_op_toggle = state.clone();
1383 let op_button = div()
1384 .h(px(26.0))
1385 .px(px(8.0))
1386 .flex()
1387 .items_center()
1388 .border_1()
1389 .border_color(c_line)
1390 .bg(c_bg)
1391 .cursor_pointer()
1392 .child(panel.current_op_label())
1393 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1394 st_op_toggle.update(cx, |s, cx| {
1395 s.toggle_filter_op_menu();
1396 cx.notify();
1397 });
1398 });
1399
1400 let op_menu = panel.op_menu_open.then(|| {
1401 let mut items: Vec<gpui::AnyElement> = Vec::new();
1402 for (i, label) in panel.op_labels().iter().enumerate() {
1403 let selected = i == panel.op_index;
1404 let st_pick = state.clone();
1405 items.push(
1406 div()
1407 .h(px(24.0))
1408 .px(px(8.0))
1409 .flex()
1410 .items_center()
1411 .bg(if selected { c_accent } else { c_bg })
1412 .cursor_pointer()
1413 .child(*label)
1414 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1415 st_pick.update(cx, |s, cx| {
1416 s.set_filter_operator(i);
1417 cx.notify();
1418 });
1419 })
1420 .into_any_element(),
1421 );
1422 }
1423 div()
1424 .flex()
1425 .flex_col()
1426 .border_1()
1427 .border_color(c_line)
1428 .bg(c_bg)
1429 .children(items)
1430 });
1431
1432 let operand_field = |value: &str, focused: bool, placeholder: &str, input: FilterInput| {
1434 let st_focus = state.clone();
1435 let (text, is_placeholder) = if value.is_empty() {
1436 (placeholder.to_owned(), true)
1437 } else {
1438 (value.to_owned(), false)
1439 };
1440 div()
1441 .h(px(26.0))
1442 .px(px(6.0))
1443 .flex()
1444 .items_center()
1445 .gap(px(2.0))
1446 .border_1()
1447 .border_color(if focused { c_accent } else { c_line })
1448 .bg(c_bg)
1449 .cursor_pointer()
1450 .child(
1451 div()
1452 .text_color(if is_placeholder { c_muted } else { c_fg })
1453 .child(text),
1454 )
1455 .children(focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
1456 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1457 st_focus.update(cx, |s, cx| {
1458 s.set_filter_focus(input);
1459 cx.notify();
1460 });
1461 })
1462 };
1463
1464 let operand_placeholder = if panel.kind == crate::data::ColumnKind::Date {
1465 "YYYY-MM-DD"
1466 } else if crate::filter::uses_number_ops(panel.kind) {
1467 "value"
1468 } else if panel.op_index == 7 {
1469 "regex"
1471 } else {
1472 "value"
1473 };
1474 let operands = panel.needs_operand().then(|| {
1475 let mut row = div().flex().flex_col().gap(px(4.0)).child(operand_field(
1476 &panel.operand_a.value,
1477 panel.focus == FilterInput::OperandA,
1478 operand_placeholder,
1479 FilterInput::OperandA,
1480 ));
1481 if panel.needs_second_operand() {
1482 row = row
1483 .child(div().text_color(c_muted).text_size(px(11.0)).child("and"))
1484 .child(operand_field(
1485 &panel.operand_b.value,
1486 panel.focus == FilterInput::OperandB,
1487 operand_placeholder,
1488 FilterInput::OperandB,
1489 ));
1490 }
1491 row
1492 });
1493
1494 let st_search = state.clone();
1496 let search_focused = panel.focus == FilterInput::Search;
1497 let (search_text, search_is_ph) = if panel.search.value.is_empty() {
1498 ("Search".to_owned(), true)
1499 } else {
1500 (panel.search.value.clone(), false)
1501 };
1502 let search_box = div()
1503 .h(px(26.0))
1504 .px(px(6.0))
1505 .flex()
1506 .items_center()
1507 .gap(px(2.0))
1508 .border_1()
1509 .border_color(if search_focused { c_accent } else { c_line })
1510 .bg(c_bg)
1511 .cursor_pointer()
1512 .child(
1513 div()
1514 .text_color(if search_is_ph { c_muted } else { c_fg })
1515 .child(search_text),
1516 )
1517 .children(search_focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
1518 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1519 st_search.update(cx, |s, cx| {
1520 s.set_filter_focus(FilterInput::Search);
1521 cx.notify();
1522 });
1523 });
1524
1525 let st_all = state.clone();
1527 let select_all_row = div()
1528 .h(px(24.0))
1529 .flex()
1530 .items_center()
1531 .gap(px(6.0))
1532 .cursor_pointer()
1533 .child(checkbox(panel.all_checked()))
1534 .child("(Select All)")
1535 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1536 st_all.update(cx, |s, cx| {
1537 s.toggle_filter_select_all();
1538 cx.notify();
1539 });
1540 });
1541
1542 let visible = panel.visible_indices();
1543 let mut value_rows: Vec<gpui::AnyElement> = Vec::new();
1544 for &idx in visible.iter().take(FILTER_PANEL_MAX_ROWS) {
1545 let row = &panel.distinct[idx];
1546 let st_val = state.clone();
1547 value_rows.push(
1548 div()
1549 .h(px(22.0))
1550 .flex()
1551 .items_center()
1552 .gap(px(6.0))
1553 .cursor_pointer()
1554 .child(checkbox(row.checked))
1555 .child(div().text_color(c_fg).child(row.label.clone()))
1556 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1557 st_val.update(cx, |s, cx| {
1558 s.toggle_filter_value(idx);
1559 cx.notify();
1560 });
1561 })
1562 .into_any_element(),
1563 );
1564 }
1565 let truncated = visible.len() > FILTER_PANEL_MAX_ROWS;
1566 let value_list = div()
1567 .id("filter-value-list")
1568 .flex()
1569 .flex_col()
1570 .max_h(px(180.0))
1571 .overflow_y_scroll()
1572 .children(value_rows)
1573 .children(truncated.then(|| {
1574 div()
1575 .text_color(c_muted)
1576 .text_size(px(11.0))
1577 .child("Refine search to see more…")
1578 }));
1579
1580 let st_clear = state.clone();
1582 let st_close = state.clone();
1583 let clear_bg = if filter_active { c_hover } else { c_bg };
1584 let clear_fg = if filter_active { c_fg } else { c_muted };
1585 let clear_border = if filter_active { c_line } else { c_muted };
1586 let buttons_row = div()
1587 .flex()
1588 .gap(px(6.0))
1589 .child(
1590 div()
1591 .flex_1()
1592 .h(px(28.0))
1593 .flex()
1594 .items_center()
1595 .justify_center()
1596 .border_1()
1597 .border_color(clear_border)
1598 .bg(clear_bg)
1599 .text_color(clear_fg)
1600 .cursor_pointer()
1601 .child("Clear Filter")
1602 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1603 if !filter_active {
1604 return;
1605 }
1606 st_clear.update(cx, |s, cx| {
1607 s.clear_filter_panel();
1608 cx.notify();
1609 });
1610 }),
1611 )
1612 .child(
1613 div()
1614 .flex_1()
1615 .h(px(28.0))
1616 .flex()
1617 .items_center()
1618 .justify_center()
1619 .border_1()
1620 .border_color(c_line)
1621 .bg(c_hover)
1622 .cursor_pointer()
1623 .child("Close")
1624 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
1625 st_close.update(cx, |s, cx| {
1626 s.filter_panel = None;
1627 cx.notify();
1628 });
1629 }),
1630 );
1631
1632 let panel_body = div()
1633 .flex()
1634 .flex_col()
1635 .w(px(FILTER_PANEL_WIDTH))
1636 .p(px(10.0))
1637 .gap(px(8.0))
1638 .bg(c_bg)
1639 .border_1()
1640 .border_color(c_line)
1641 .text_color(c_fg)
1642 .text_size(px(13.0))
1643 .child(div().text_color(c_muted).text_size(px(11.0)).child("Sort"))
1644 .child(sort_row)
1645 .child(
1646 div()
1647 .text_color(c_muted)
1648 .text_size(px(11.0))
1649 .child("Filter"),
1650 )
1651 .child(op_button)
1652 .children(op_menu)
1653 .children(operands)
1654 .child(search_box)
1655 .child(select_all_row)
1656 .child(value_list)
1657 .child(buttons_row);
1658
1659 let st_backdrop = state.clone();
1660 let overlay = deferred(
1661 anchored()
1662 .anchor(Corner::BottomLeft)
1663 .position(point(px(abs_x), px(abs_y)))
1664 .child(div().occlude().child(panel_body).on_mouse_down_out(
1665 move |_e: &MouseDownEvent, _window, cx| {
1666 st_backdrop.update(cx, |s, cx| {
1667 if s.filter_panel.is_some() {
1668 s.filter_panel = None;
1669 cx.notify();
1670 }
1671 });
1672 },
1673 )),
1674 )
1675 .with_priority(CONTEXT_MENU_PRIORITY);
1676
1677 Some(overlay)
1678}
1679
1680fn render_busy_overlay(
1686 state: &Entity<GridState>,
1687 cx: &mut Context<SqllyDataTable>,
1688) -> Option<impl IntoElement> {
1689 let s = state.read(cx);
1690 let busy = s.busy.clone()?;
1691 let theme = s.theme.clone();
1692 let track = theme.grid_line;
1693 let accent = theme.sort_indicator;
1694
1695 let bar: gpui::AnyElement = if let Some(p) = busy.progress {
1696 let p = p.clamp(0.0, 1.0);
1697 div()
1698 .h_full()
1699 .w(relative(p))
1700 .rounded(px(3.0))
1701 .bg(accent)
1702 .into_any_element()
1703 } else {
1704 div()
1705 .h_full()
1706 .w(relative(0.3))
1707 .rounded(px(3.0))
1708 .bg(accent)
1709 .with_animation(
1710 "busy-indeterminate",
1711 Animation::new(std::time::Duration::from_millis(900))
1712 .repeat()
1713 .with_easing(pulsating_between(0.15, 0.85)),
1714 |el, delta| el.w(relative(delta)),
1715 )
1716 .into_any_element()
1717 };
1718
1719 let card = div()
1720 .flex()
1721 .flex_col()
1722 .gap(px(10.0))
1723 .p(px(16.0))
1724 .min_w(px(220.0))
1725 .rounded(px(8.0))
1726 .bg(theme.menu_bg)
1727 .border_1()
1728 .border_color(theme.grid_line)
1729 .child(
1730 div()
1731 .text_color(theme.menu_fg)
1732 .text_size(px(14.0))
1733 .child(busy.label.clone()),
1734 )
1735 .child(
1736 div()
1737 .w_full()
1738 .h(px(6.0))
1739 .rounded(px(3.0))
1740 .bg(track)
1741 .child(bar),
1742 );
1743
1744 let overlay = div()
1745 .absolute()
1746 .top_0()
1747 .left_0()
1748 .size_full()
1749 .occlude()
1750 .flex()
1751 .items_center()
1752 .justify_center()
1753 .bg(theme.overlay_scrim)
1754 .child(card);
1755
1756 Some(overlay)
1757}
1758
1759enum MenuDispatch {
1762 Builtin(menu::MenuAction, usize),
1763 Custom(
1764 String,
1765 Option<crate::grid::context_menu::ContextMenuRequest>,
1766 ),
1767}