1use crate::config::GridConfig;
7use crate::data::GridData;
8use crate::grid::context_menu::{
9 ContextMenuProvider, ContextMenuProviderHandle, PendingCustomContextMenuAction,
10};
11use crate::grid::paint::{paint_grid, paint_status_bar, PaintData, StatusBarData};
12use crate::grid::state::state_inner;
13use crate::grid::state::{FilterInput, GridState, EDGE_SCROLL_TICK_MS};
14use crate::grid::theme::GridTheme;
15use crate::grid::{menu, HitResult, MenuItem, SortDirection};
16
17use gpui::{
18 anchored, canvas, deferred, div, hsla, point, pulsating_between, px, relative, Animation,
19 AnimationExt, App, AppContext, Context, Corner, Entity, FocusHandle, Focusable,
20 InteractiveElement, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
21 MouseUpEvent, ParentElement, Render, ScrollWheelEvent, StatefulInteractiveElement, Styled,
22 Window,
23};
24
25const CONTEXT_MENU_PRIORITY: usize = 1_000_000;
32
33pub struct SqllyDataTable {
35 pub state: Entity<GridState>,
36 follow_system_appearance: bool,
40 appearance_subscription: Option<gpui::Subscription>,
44}
45
46impl SqllyDataTable {
47 #[must_use]
49 pub fn new(state: Entity<GridState>) -> Self {
50 Self {
51 state,
52 follow_system_appearance: true,
53 appearance_subscription: None,
54 }
55 }
56
57 #[must_use]
59 pub fn builder(data: GridData) -> SqllyDataTableBuilder {
60 SqllyDataTableBuilder {
61 data,
62 config: GridConfig::default(),
63 context_menu_provider: None,
64 theme: None,
65 debug_bar: false,
66 }
67 }
68}
69
70pub struct SqllyDataTableBuilder {
72 data: GridData,
73 config: GridConfig,
74 context_menu_provider: Option<ContextMenuProviderHandle>,
75 theme: Option<GridTheme>,
76 debug_bar: bool,
77}
78
79impl SqllyDataTableBuilder {
80 #[must_use]
82 pub fn config(mut self, config: GridConfig) -> Self {
83 self.config = config;
84 self
85 }
86
87 #[must_use]
90 pub fn theme(mut self, theme: GridTheme) -> Self {
91 self.theme = Some(theme);
92 self
93 }
94
95 #[must_use]
102 pub fn context_menu_provider(mut self, provider: impl ContextMenuProvider + 'static) -> Self {
103 self.context_menu_provider = Some(ContextMenuProviderHandle::new(provider));
104 self
105 }
106
107 #[must_use]
111 pub fn debug_bar(mut self, enabled: bool) -> Self {
112 self.debug_bar = enabled;
113 self
114 }
115
116 pub fn build(self, cx: &mut App) -> SqllyDataTable {
118 let focus = cx.focus_handle();
119 let provider = self.context_menu_provider;
120 let theme_override = self.theme;
121 let debug_bar = self.debug_bar;
122 let follow_system_appearance = theme_override.is_none();
123 let state = cx.new(|cx| {
124 let mut s = GridState::new(self.data, self.config, focus.clone());
125 s.context_menu_provider = provider;
126 s.debug_bar_enabled = debug_bar;
127 s.self_weak = Some(cx.weak_entity());
128 if let Some(theme) = theme_override {
129 s.theme = theme;
130 }
131 s
132 });
133 SqllyDataTable {
134 state,
135 follow_system_appearance,
136 appearance_subscription: None,
137 }
138 }
139}
140
141impl Focusable for SqllyDataTable {
142 fn focus_handle(&self, cx: &App) -> FocusHandle {
143 self.state.read(cx).focus_handle.clone()
144 }
145}
146
147impl Render for SqllyDataTable {
148 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
149 if self.follow_system_appearance && self.appearance_subscription.is_none() {
154 let initial = GridTheme::for_appearance(window.appearance());
155 self.state.update(cx, |s, _cx| s.theme = initial);
156 let state_appearance = self.state.clone();
157 self.appearance_subscription =
158 Some(window.observe_window_appearance(move |window, cx| {
159 let theme = GridTheme::for_appearance(window.appearance());
160 state_appearance.update(cx, |s, cx| {
161 s.theme = theme;
162 cx.notify();
163 });
164 }));
165 }
166
167 let state_canvas = self.state.clone();
168 let state_status = self.state.clone();
169 let state_mouse = self.state.clone();
170 let state_move = self.state.clone();
171 let state_up = self.state.clone();
172 let state_scroll = self.state.clone();
173 let state_key = self.state.clone();
174 let state_right = self.state.clone();
175 let bg = self.state.read(cx).theme.bg;
176 let focus_handle = self.state.read(cx).focus_handle.clone();
177 let focus_left = focus_handle.clone();
178 let focus_right = focus_handle.clone();
179 let debug_bar = self.state.read(cx).debug_bar_enabled;
180 let status_h = self.state.read(cx).status_bar_height;
181
182 if let Some((action, col)) = self.state.read(cx).pending_action {
185 self.state.update(cx, |s, cx| {
186 s.execute_action(action, col, cx);
187 s.pending_action = None;
188 });
189 }
190
191 if let Some(pending) = self
193 .state
194 .read(cx)
195 .pending_custom_context_menu_action
196 .clone()
197 {
198 self.state.update(cx, |s, cx| {
199 s.pending_custom_context_menu_action = None;
200 s.execute_custom_context_menu_action(pending, cx);
201 });
202 }
203
204 if self.state.read(cx).is_dragging && !self.state.read(cx).edge_scroll_active {
212 self.state.update(cx, |s, _cx| s.edge_scroll_active = true);
213 let state_edge = self.state.clone();
214 cx.spawn(async move |_weak, cx| {
215 loop {
216 gpui::Timer::after(std::time::Duration::from_millis(EDGE_SCROLL_TICK_MS)).await;
217 let res = cx.update(|cx| state_edge.update(cx, |s, _cx| s.apply_edge_scroll()));
218 if let Ok(true) = res {
219 let _ = state_edge.update(cx, |_s, cx| cx.notify());
220 }
221 let dragging_res = cx.update(|cx| state_edge.read(cx).is_dragging);
222 if !matches!(dragging_res, Ok(true)) {
223 break;
224 }
225 }
226 let _ =
227 cx.update(|cx| state_edge.update(cx, |s, _cx| s.edge_scroll_active = false));
228 })
229 .detach();
230 }
231
232 div()
233 .flex()
234 .flex_col()
235 .size_full()
236 .relative()
237 .track_focus(&focus_handle)
238 .bg(bg)
239 .child(
240 canvas(
241 move |bounds, window, cx| -> PaintData {
242 let viewport = window.viewport_size();
243 state_canvas.update(cx, |s, cx| {
244 let mut dirty = false;
245 if s.bounds != bounds {
246 s.bounds = bounds;
247 s.clamp_scroll_to_bounds();
248 dirty = true;
249 }
250 if s.window_viewport != viewport {
251 s.window_viewport = viewport;
252 }
253 if dirty {
254 cx.notify();
255 }
256 });
257 let s = state_canvas.read(cx);
258 PaintData::from_state(s)
259 },
260 move |bounds, data, window, cx| {
261 paint_grid(&data, window, cx, bounds);
262 },
263 )
264 .flex_1(),
265 )
266 .children(debug_bar.then(|| {
267 canvas(
268 move |_bounds, _window, cx| -> StatusBarData {
269 let s = state_status.read(cx);
270 StatusBarData::from_state(s)
271 },
272 move |bounds, data, window, cx| {
273 paint_status_bar(&data, window, cx, bounds);
274 },
275 )
276 .h(px(status_h))
277 }))
278 .children(render_context_menu_overlay(&self.state, cx))
279 .children(render_filter_panel_overlay(&self.state, cx))
280 .children(render_busy_overlay(&self.state, cx))
281 .on_mouse_down(
282 MouseButton::Left,
283 move |event: &MouseDownEvent, window, cx| {
284 window.focus(&focus_left);
285 state_mouse.update(cx, |s, cx| {
286 if s.busy.is_some() {
289 return;
290 }
291 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
297 if s.context_menu.is_some() || s.filter_panel.is_some() {
298 s.context_menu = None;
299 s.filter_panel = None;
300 }
301 s.handle_mouse_down_with_modifiers(
302 rel,
303 event.modifiers.shift,
304 event.modifiers.platform,
305 );
306 cx.notify();
307 });
308 },
309 )
310 .on_mouse_down(
311 MouseButton::Right,
312 move |event: &MouseDownEvent, window, cx| {
313 window.focus(&focus_right);
314 state_right.update(cx, |s, cx| {
315 if s.busy.is_some() {
316 return;
317 }
318 let pos = state_inner::to_grid_relative(event.position, s.bounds.origin);
319 let hit = s.hit_test(pos);
320
321 if s.context_menu_provider.is_none() {
323 match hit {
324 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
325 s.open_context_menu(col, pos);
326 }
327 _ => {
328 s.context_menu = None;
329 s.filter_panel = None;
330 }
331 }
332 cx.notify();
333 return;
334 }
335
336 let Some(target) = s.context_menu_target_from_hit(hit) else {
338 s.context_menu = None;
339 s.filter_panel = None;
340 cx.notify();
341 return;
342 };
343
344 let effective = s.effective_selection_for_context_target(&target);
345 if effective != s.selection {
346 s.selection = effective.clone();
347 }
348
349 let request = s.build_context_menu_request(target, &effective);
350 let col = request.target.column_index().unwrap_or(0);
351
352 let Some(provider) = s.context_menu_provider.clone() else {
353 return;
354 };
355 let public_items = provider.menu_items(&request);
356 let items = GridState::convert_context_menu_items(public_items);
357
358 if items.is_empty() {
359 s.context_menu = None;
360 } else {
361 s.context_menu =
362 Some(menu::ContextMenu::custom(col, pos, items, request));
363 }
364 s.filter_panel = None;
365 cx.notify();
366 });
367 },
368 )
369 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
370 state_move.update(cx, |s, cx| {
371 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
372 s.handle_mouse_move(rel, event.pressed_button);
373 cx.notify();
374 });
375 })
376 .on_mouse_up(
377 MouseButton::Left,
378 move |_event: &MouseUpEvent, _window, cx| {
379 state_up.update(cx, |s, cx| {
380 s.handle_mouse_up();
381 cx.notify();
382 });
383 },
384 )
385 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
386 state_scroll.update(cx, |s, cx| {
387 let line_h = px(s.row_height);
388 let delta = event.delta.pixel_delta(line_h);
389 let scroll = s.scroll_handle.offset();
390 let (mx, my) = s.max_scroll();
391 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
392 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
393 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
394 if s.drag_start.is_some() {
395 s.handle_scroll_drag();
396 }
397 cx.notify();
398 });
399 })
400 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
401 let ks = &event.keystroke;
402 if ks.modifiers.platform && ks.key == "q" {
403 cx.quit();
404 return;
405 }
406 state_key.update(cx, |s, cx| {
407 let kb = &s.config.key_bindings;
408 if kb.select_all.matches(ks) {
409 s.select_all();
410 } else if kb.copy.matches(ks) {
411 s.copy_selection(false, cx);
412 } else if kb.copy_with_headers.matches(ks) {
413 s.copy_selection(true, cx);
414 } else if kb.page_up.matches(ks) {
415 s.page_up();
416 } else if kb.page_down.matches(ks) {
417 s.page_down();
418 } else {
419 s.handle_key(ks);
420 }
421 cx.notify();
422 });
423 })
424 }
425}
426
427fn render_context_menu_overlay(
439 state: &Entity<GridState>,
440 cx: &mut Context<SqllyDataTable>,
441) -> Option<impl IntoElement> {
442 let s = state.read(cx);
443 let menu = s.context_menu.clone()?;
444 let theme = s.theme.clone();
445 let cw = s.char_width;
446 let grid_ox = f32::from(s.bounds.origin.x);
447 let grid_oy = f32::from(s.bounds.origin.y);
448 let viewport = s.window_viewport;
449 let vw = f32::from(viewport.width);
450 let vh = f32::from(viewport.height);
451
452 let resolved = menu.resolved_position(grid_ox, grid_oy, vw, vh, cw);
453 let abs_x = grid_ox + f32::from(resolved.x);
454 let abs_y = grid_oy + f32::from(resolved.y);
455 let menu_w = menu.width_for(cw);
456
457 let mut rows: Vec<gpui::AnyElement> = Vec::with_capacity(menu.items.len());
460 let mut selectable_idx = 0usize;
461 for item in &menu.items {
462 match item {
463 MenuItem::Separator => {
464 rows.push(
465 div()
466 .h(px(menu::MENU_ITEM_HEIGHT))
467 .flex()
468 .items_center()
469 .child(div().mx(px(4.0)).h(px(1.0)).w_full().bg(theme.grid_line))
470 .into_any_element(),
471 );
472 }
473 MenuItem::Action(_) | MenuItem::Custom { .. } => {
474 let this_idx = selectable_idx;
475 selectable_idx += 1;
476 let label = item.label().unwrap_or("").to_owned();
477 let hovered = menu.hovered == Some(this_idx);
478
479 let action = match item {
483 MenuItem::Action(a) => MenuDispatch::Builtin(*a, menu.col),
484 MenuItem::Custom { id, .. } => {
485 MenuDispatch::Custom(id.clone(), menu.request.clone())
486 }
487 MenuItem::Separator => unreachable!(),
488 };
489
490 let state_click = state.clone();
491 let state_hover = state.clone();
492 let mut row = div()
493 .h(px(menu::MENU_ITEM_HEIGHT))
494 .px(px(menu::MENU_PADDING_X))
495 .flex()
496 .items_center()
497 .text_color(theme.menu_fg)
498 .text_size(px(menu::MENU_FONT_SIZE))
499 .child(label)
500 .on_mouse_move(move |_e: &MouseMoveEvent, _window, cx| {
501 state_hover.update(cx, |s, cx| {
502 if let Some(m) = s.context_menu.as_mut() {
503 if m.hovered != Some(this_idx) {
504 m.hovered = Some(this_idx);
505 cx.notify();
506 }
507 }
508 });
509 })
510 .on_mouse_down(
511 MouseButton::Left,
512 move |_e: &MouseDownEvent, _window, cx| {
513 state_click.update(cx, |s, cx| {
514 match &action {
515 MenuDispatch::Builtin(a, col) => {
516 s.pending_action = Some((*a, *col));
517 }
518 MenuDispatch::Custom(id, request) => {
519 if let Some(request) = request {
520 s.pending_custom_context_menu_action =
521 Some(PendingCustomContextMenuAction {
522 id: id.clone(),
523 request: request.clone(),
524 });
525 }
526 }
527 }
528 s.context_menu = None;
529 cx.notify();
530 });
531 },
532 );
533 if hovered {
534 row = row.bg(theme.menu_hover_bg);
535 }
536 rows.push(row.into_any_element());
537 }
538 }
539 }
540
541 let menu_body = div()
542 .flex()
543 .flex_col()
544 .w(px(menu_w))
545 .py(px(menu::MENU_INNER_PAD))
546 .bg(theme.menu_bg)
547 .border_1()
548 .border_color(theme.grid_line)
549 .children(rows);
550
551 let state_backdrop = state.clone();
554 let overlay = deferred(anchored().position(point(px(abs_x), px(abs_y))).child(
555 div().occlude().child(menu_body).on_mouse_down_out(
556 move |_e: &MouseDownEvent, _window, cx| {
557 state_backdrop.update(cx, |s, cx| {
558 if s.context_menu.is_some() {
559 s.context_menu = None;
560 s.filter_panel = None;
561 cx.notify();
562 }
563 });
564 },
565 ),
566 ))
567 .with_priority(CONTEXT_MENU_PRIORITY);
568
569 Some(overlay)
570}
571
572const FILTER_PANEL_WIDTH: f32 = 300.0;
574const FILTER_PANEL_MAX_ROWS: usize = 200;
576
577#[allow(clippy::too_many_lines)]
582fn render_filter_panel_overlay(
583 state: &Entity<GridState>,
584 cx: &mut Context<SqllyDataTable>,
585) -> Option<impl IntoElement> {
586 let s = state.read(cx);
587 let panel = s.filter_panel.clone()?;
588 let theme = s.theme.clone();
589 let col = panel.col;
590 let current_sort = s.sort;
591 let filter_active = s.filters.get(col).is_some_and(|f| f.is_active());
592 let grid_ox = f32::from(s.bounds.origin.x);
593 let grid_oy = f32::from(s.bounds.origin.y);
594
595 let abs_x = grid_ox + f32::from(panel.anchor.x);
600 let abs_y = grid_oy + f32::from(panel.anchor.y);
601
602 let c_bg = theme.menu_bg;
604 let c_line = theme.grid_line;
605 let c_fg = theme.menu_fg;
606 let c_accent = theme.sort_indicator;
607 let c_hover = theme.menu_hover_bg;
608 let c_muted = theme.muted_text;
609
610 let checkbox = move |checked: bool| {
611 let mut b = div()
612 .w(px(14.0))
613 .h(px(14.0))
614 .border_1()
615 .border_color(c_line)
616 .bg(c_bg)
617 .flex()
618 .items_center()
619 .justify_center();
620 if checked {
621 b = b.child(div().w(px(8.0)).h(px(8.0)).bg(c_accent));
622 }
623 b
624 };
625
626 let (asc_active, desc_active) = match current_sort {
628 Some((c, SortDirection::Ascending)) if c == col => (true, false),
629 Some((c, SortDirection::Descending)) if c == col => (false, true),
630 _ => (false, false),
631 };
632 let st_asc = state.clone();
633 let st_desc = state.clone();
634 let sort_row = div()
635 .flex()
636 .gap(px(6.0))
637 .child(
638 div()
639 .flex_1()
640 .h(px(26.0))
641 .flex()
642 .items_center()
643 .justify_center()
644 .border_1()
645 .border_color(c_line)
646 .bg(if asc_active { c_accent } else { c_hover })
647 .cursor_pointer()
648 .child("Ascending")
649 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
650 st_asc.update(cx, |s, cx| {
651 s.set_panel_sort(SortDirection::Ascending);
652 cx.notify();
653 });
654 }),
655 )
656 .child(
657 div()
658 .flex_1()
659 .h(px(26.0))
660 .flex()
661 .items_center()
662 .justify_center()
663 .border_1()
664 .border_color(c_line)
665 .bg(if desc_active { c_accent } else { c_hover })
666 .cursor_pointer()
667 .child("Descending")
668 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
669 st_desc.update(cx, |s, cx| {
670 s.set_panel_sort(SortDirection::Descending);
671 cx.notify();
672 });
673 }),
674 );
675
676 let st_op_toggle = state.clone();
678 let op_button = div()
679 .h(px(26.0))
680 .px(px(8.0))
681 .flex()
682 .items_center()
683 .border_1()
684 .border_color(c_line)
685 .bg(c_bg)
686 .cursor_pointer()
687 .child(panel.current_op_label())
688 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
689 st_op_toggle.update(cx, |s, cx| {
690 s.toggle_filter_op_menu();
691 cx.notify();
692 });
693 });
694
695 let op_menu = panel.op_menu_open.then(|| {
696 let mut items: Vec<gpui::AnyElement> = Vec::new();
697 for (i, label) in panel.op_labels().iter().enumerate() {
698 let selected = i == panel.op_index;
699 let st_pick = state.clone();
700 items.push(
701 div()
702 .h(px(24.0))
703 .px(px(8.0))
704 .flex()
705 .items_center()
706 .bg(if selected { c_accent } else { c_bg })
707 .cursor_pointer()
708 .child(*label)
709 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
710 st_pick.update(cx, |s, cx| {
711 s.set_filter_operator(i);
712 cx.notify();
713 });
714 })
715 .into_any_element(),
716 );
717 }
718 div()
719 .flex()
720 .flex_col()
721 .border_1()
722 .border_color(c_line)
723 .bg(c_bg)
724 .children(items)
725 });
726
727 let operand_field = |value: &str, focused: bool, placeholder: &str, input: FilterInput| {
729 let st_focus = state.clone();
730 let (text, is_placeholder) = if value.is_empty() {
731 (placeholder.to_owned(), true)
732 } else {
733 (value.to_owned(), false)
734 };
735 div()
736 .h(px(26.0))
737 .px(px(6.0))
738 .flex()
739 .items_center()
740 .gap(px(2.0))
741 .border_1()
742 .border_color(if focused { c_accent } else { c_line })
743 .bg(c_bg)
744 .cursor_pointer()
745 .child(
746 div()
747 .text_color(if is_placeholder { c_muted } else { c_fg })
748 .child(text),
749 )
750 .children(focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
751 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
752 st_focus.update(cx, |s, cx| {
753 s.set_filter_focus(input);
754 cx.notify();
755 });
756 })
757 };
758
759 let operand_placeholder = if panel.kind == crate::data::ColumnKind::Date {
760 "YYYY-MM-DD"
761 } else if crate::filter::uses_number_ops(panel.kind) {
762 "value"
763 } else if panel.op_index == 7 {
764 "regex"
766 } else {
767 "value"
768 };
769 let operands = panel.needs_operand().then(|| {
770 let mut row = div().flex().flex_col().gap(px(4.0)).child(operand_field(
771 &panel.operand_a.value,
772 panel.focus == FilterInput::OperandA,
773 operand_placeholder,
774 FilterInput::OperandA,
775 ));
776 if panel.needs_second_operand() {
777 row = row
778 .child(div().text_color(c_muted).text_size(px(11.0)).child("and"))
779 .child(operand_field(
780 &panel.operand_b.value,
781 panel.focus == FilterInput::OperandB,
782 operand_placeholder,
783 FilterInput::OperandB,
784 ));
785 }
786 row
787 });
788
789 let st_search = state.clone();
791 let search_focused = panel.focus == FilterInput::Search;
792 let (search_text, search_is_ph) = if panel.search.value.is_empty() {
793 ("Search".to_owned(), true)
794 } else {
795 (panel.search.value.clone(), false)
796 };
797 let search_box = div()
798 .h(px(26.0))
799 .px(px(6.0))
800 .flex()
801 .items_center()
802 .gap(px(2.0))
803 .border_1()
804 .border_color(if search_focused { c_accent } else { c_line })
805 .bg(c_bg)
806 .cursor_pointer()
807 .child(
808 div()
809 .text_color(if search_is_ph { c_muted } else { c_fg })
810 .child(search_text),
811 )
812 .children(search_focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
813 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
814 st_search.update(cx, |s, cx| {
815 s.set_filter_focus(FilterInput::Search);
816 cx.notify();
817 });
818 });
819
820 let st_all = state.clone();
822 let select_all_row = div()
823 .h(px(24.0))
824 .flex()
825 .items_center()
826 .gap(px(6.0))
827 .cursor_pointer()
828 .child(checkbox(panel.all_checked()))
829 .child("(Select All)")
830 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
831 st_all.update(cx, |s, cx| {
832 s.toggle_filter_select_all();
833 cx.notify();
834 });
835 });
836
837 let visible = panel.visible_indices();
838 let mut value_rows: Vec<gpui::AnyElement> = Vec::new();
839 for &idx in visible.iter().take(FILTER_PANEL_MAX_ROWS) {
840 let row = &panel.distinct[idx];
841 let st_val = state.clone();
842 value_rows.push(
843 div()
844 .h(px(22.0))
845 .flex()
846 .items_center()
847 .gap(px(6.0))
848 .cursor_pointer()
849 .child(checkbox(row.checked))
850 .child(div().text_color(c_fg).child(row.label.clone()))
851 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
852 st_val.update(cx, |s, cx| {
853 s.toggle_filter_value(idx);
854 cx.notify();
855 });
856 })
857 .into_any_element(),
858 );
859 }
860 let truncated = visible.len() > FILTER_PANEL_MAX_ROWS;
861 let value_list = div()
862 .id("filter-value-list")
863 .flex()
864 .flex_col()
865 .max_h(px(180.0))
866 .overflow_y_scroll()
867 .children(value_rows)
868 .children(truncated.then(|| {
869 div()
870 .text_color(c_muted)
871 .text_size(px(11.0))
872 .child("Refine search to see more…")
873 }));
874
875 let st_clear = state.clone();
877 let st_close = state.clone();
878 let clear_bg = if filter_active { c_hover } else { c_bg };
879 let clear_fg = if filter_active { c_fg } else { c_muted };
880 let clear_border = if filter_active { c_line } else { c_muted };
881 let buttons_row = div()
882 .flex()
883 .gap(px(6.0))
884 .child(
885 div()
886 .flex_1()
887 .h(px(28.0))
888 .flex()
889 .items_center()
890 .justify_center()
891 .border_1()
892 .border_color(clear_border)
893 .bg(clear_bg)
894 .text_color(clear_fg)
895 .cursor_pointer()
896 .child("Clear Filter")
897 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
898 if !filter_active {
899 return;
900 }
901 st_clear.update(cx, |s, cx| {
902 s.clear_filter_panel();
903 cx.notify();
904 });
905 }),
906 )
907 .child(
908 div()
909 .flex_1()
910 .h(px(28.0))
911 .flex()
912 .items_center()
913 .justify_center()
914 .border_1()
915 .border_color(c_line)
916 .bg(c_hover)
917 .cursor_pointer()
918 .child("Close")
919 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
920 st_close.update(cx, |s, cx| {
921 s.filter_panel = None;
922 cx.notify();
923 });
924 }),
925 );
926
927 let panel_body = div()
928 .flex()
929 .flex_col()
930 .w(px(FILTER_PANEL_WIDTH))
931 .p(px(10.0))
932 .gap(px(8.0))
933 .bg(c_bg)
934 .border_1()
935 .border_color(c_line)
936 .text_color(c_fg)
937 .text_size(px(13.0))
938 .child(div().text_color(c_muted).text_size(px(11.0)).child("Sort"))
939 .child(sort_row)
940 .child(
941 div()
942 .text_color(c_muted)
943 .text_size(px(11.0))
944 .child("Filter"),
945 )
946 .child(op_button)
947 .children(op_menu)
948 .children(operands)
949 .child(search_box)
950 .child(select_all_row)
951 .child(value_list)
952 .child(buttons_row);
953
954 let st_backdrop = state.clone();
955 let overlay = deferred(
956 anchored()
957 .anchor(Corner::BottomLeft)
958 .position(point(px(abs_x), px(abs_y)))
959 .child(div().occlude().child(panel_body).on_mouse_down_out(
960 move |_e: &MouseDownEvent, _window, cx| {
961 st_backdrop.update(cx, |s, cx| {
962 if s.filter_panel.is_some() {
963 s.filter_panel = None;
964 cx.notify();
965 }
966 });
967 },
968 )),
969 )
970 .with_priority(CONTEXT_MENU_PRIORITY);
971
972 Some(overlay)
973}
974
975fn render_busy_overlay(
981 state: &Entity<GridState>,
982 cx: &mut Context<SqllyDataTable>,
983) -> Option<impl IntoElement> {
984 let s = state.read(cx);
985 let busy = s.busy.clone()?;
986 let theme = s.theme.clone();
987 let track = theme.grid_line;
988 let accent = theme.sort_indicator;
989
990 let bar: gpui::AnyElement = if let Some(p) = busy.progress {
991 let p = p.clamp(0.0, 1.0);
992 div()
993 .h_full()
994 .w(relative(p))
995 .rounded(px(3.0))
996 .bg(accent)
997 .into_any_element()
998 } else {
999 div()
1000 .h_full()
1001 .w(relative(0.3))
1002 .rounded(px(3.0))
1003 .bg(accent)
1004 .with_animation(
1005 "busy-indeterminate",
1006 Animation::new(std::time::Duration::from_millis(900))
1007 .repeat()
1008 .with_easing(pulsating_between(0.15, 0.85)),
1009 |el, delta| el.w(relative(delta)),
1010 )
1011 .into_any_element()
1012 };
1013
1014 let card = div()
1015 .flex()
1016 .flex_col()
1017 .gap(px(10.0))
1018 .p(px(16.0))
1019 .min_w(px(220.0))
1020 .rounded(px(8.0))
1021 .bg(theme.menu_bg)
1022 .border_1()
1023 .border_color(theme.grid_line)
1024 .child(
1025 div()
1026 .text_color(theme.menu_fg)
1027 .text_size(px(14.0))
1028 .child(busy.label.clone()),
1029 )
1030 .child(
1031 div()
1032 .w_full()
1033 .h(px(6.0))
1034 .rounded(px(3.0))
1035 .bg(track)
1036 .child(bar),
1037 );
1038
1039 let overlay = div()
1040 .absolute()
1041 .top_0()
1042 .left_0()
1043 .size_full()
1044 .occlude()
1045 .flex()
1046 .items_center()
1047 .justify_center()
1048 .bg(hsla(0.0, 0.0, 0.0, 0.35))
1049 .child(card);
1050
1051 Some(overlay)
1052}
1053
1054enum MenuDispatch {
1057 Builtin(menu::MenuAction, usize),
1058 Custom(
1059 String,
1060 Option<crate::grid::context_menu::ContextMenuRequest>,
1061 ),
1062}