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 dirty = true;
248 }
249 if s.window_viewport != viewport {
250 s.window_viewport = viewport;
251 }
252 if dirty {
253 cx.notify();
254 }
255 });
256 let s = state_canvas.read(cx);
257 PaintData::from_state(s)
258 },
259 move |bounds, data, window, cx| {
260 paint_grid(&data, window, cx, bounds);
261 },
262 )
263 .flex_1(),
264 )
265 .children(debug_bar.then(|| {
266 canvas(
267 move |_bounds, _window, cx| -> StatusBarData {
268 let s = state_status.read(cx);
269 StatusBarData::from_state(s)
270 },
271 move |bounds, data, window, cx| {
272 paint_status_bar(&data, window, cx, bounds);
273 },
274 )
275 .h(px(status_h))
276 }))
277 .children(render_context_menu_overlay(&self.state, cx))
278 .children(render_filter_panel_overlay(&self.state, cx))
279 .children(render_busy_overlay(&self.state, cx))
280 .on_mouse_down(
281 MouseButton::Left,
282 move |event: &MouseDownEvent, window, cx| {
283 window.focus(&focus_left);
284 state_mouse.update(cx, |s, cx| {
285 if s.busy.is_some() {
288 return;
289 }
290 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
296 if s.context_menu.is_some() || s.filter_panel.is_some() {
297 s.context_menu = None;
298 s.filter_panel = None;
299 }
300 s.handle_mouse_down(rel, event.modifiers.shift);
301 cx.notify();
302 });
303 },
304 )
305 .on_mouse_down(
306 MouseButton::Right,
307 move |event: &MouseDownEvent, window, cx| {
308 window.focus(&focus_right);
309 state_right.update(cx, |s, cx| {
310 if s.busy.is_some() {
311 return;
312 }
313 let pos = state_inner::to_grid_relative(event.position, s.bounds.origin);
314 let hit = s.hit_test(pos);
315
316 if s.context_menu_provider.is_none() {
318 match hit {
319 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
320 s.open_context_menu(col, pos);
321 }
322 _ => {
323 s.context_menu = None;
324 s.filter_panel = None;
325 }
326 }
327 cx.notify();
328 return;
329 }
330
331 let Some(target) = s.context_menu_target_from_hit(hit) else {
333 s.context_menu = None;
334 s.filter_panel = None;
335 cx.notify();
336 return;
337 };
338
339 let effective = s.effective_selection_for_context_target(&target);
340 if effective != s.selection {
341 s.selection = effective.clone();
342 }
343
344 let request = s.build_context_menu_request(target, &effective);
345 let col = request.target.column_index().unwrap_or(0);
346
347 let Some(provider) = s.context_menu_provider.clone() else {
348 return;
349 };
350 let public_items = provider.menu_items(&request);
351 let items = GridState::convert_context_menu_items(public_items);
352
353 if items.is_empty() {
354 s.context_menu = None;
355 } else {
356 s.context_menu =
357 Some(menu::ContextMenu::custom(col, pos, items, request));
358 }
359 s.filter_panel = None;
360 cx.notify();
361 });
362 },
363 )
364 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
365 state_move.update(cx, |s, cx| {
366 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
367 s.handle_mouse_move(rel, event.pressed_button);
368 cx.notify();
369 });
370 })
371 .on_mouse_up(
372 MouseButton::Left,
373 move |_event: &MouseUpEvent, _window, cx| {
374 state_up.update(cx, |s, cx| {
375 s.handle_mouse_up();
376 cx.notify();
377 });
378 },
379 )
380 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
381 state_scroll.update(cx, |s, cx| {
382 let line_h = px(s.row_height);
383 let delta = event.delta.pixel_delta(line_h);
384 let scroll = s.scroll_handle.offset();
385 let (mx, my) = s.max_scroll();
386 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
387 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
388 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
389 if s.drag_start.is_some() {
390 s.handle_scroll_drag();
391 }
392 cx.notify();
393 });
394 })
395 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
396 let ks = &event.keystroke;
397 if ks.modifiers.platform && ks.key == "q" {
398 cx.quit();
399 return;
400 }
401 state_key.update(cx, |s, cx| {
402 let kb = &s.config.key_bindings;
403 if kb.select_all.matches(ks) {
404 s.select_all();
405 } else if kb.copy.matches(ks) {
406 s.copy_selection(false, cx);
407 } else if kb.copy_with_headers.matches(ks) {
408 s.copy_selection(true, cx);
409 } else if kb.page_up.matches(ks) {
410 s.page_up();
411 } else if kb.page_down.matches(ks) {
412 s.page_down();
413 } else {
414 s.handle_key(ks);
415 }
416 cx.notify();
417 });
418 })
419 }
420}
421
422fn render_context_menu_overlay(
434 state: &Entity<GridState>,
435 cx: &mut Context<SqllyDataTable>,
436) -> Option<impl IntoElement> {
437 let s = state.read(cx);
438 let menu = s.context_menu.clone()?;
439 let theme = s.theme.clone();
440 let cw = s.char_width;
441 let grid_ox = f32::from(s.bounds.origin.x);
442 let grid_oy = f32::from(s.bounds.origin.y);
443 let viewport = s.window_viewport;
444 let vw = f32::from(viewport.width);
445 let vh = f32::from(viewport.height);
446
447 let resolved = menu.resolved_position(grid_ox, grid_oy, vw, vh, cw);
448 let abs_x = grid_ox + f32::from(resolved.x);
449 let abs_y = grid_oy + f32::from(resolved.y);
450 let menu_w = menu.width_for(cw);
451
452 let mut rows: Vec<gpui::AnyElement> = Vec::with_capacity(menu.items.len());
455 let mut selectable_idx = 0usize;
456 for item in &menu.items {
457 match item {
458 MenuItem::Separator => {
459 rows.push(
460 div()
461 .h(px(menu::MENU_ITEM_HEIGHT))
462 .flex()
463 .items_center()
464 .child(div().mx(px(4.0)).h(px(1.0)).w_full().bg(theme.grid_line))
465 .into_any_element(),
466 );
467 }
468 MenuItem::Action(_) | MenuItem::Custom { .. } => {
469 let this_idx = selectable_idx;
470 selectable_idx += 1;
471 let label = item.label().unwrap_or("").to_owned();
472 let hovered = menu.hovered == Some(this_idx);
473
474 let action = match item {
478 MenuItem::Action(a) => MenuDispatch::Builtin(*a, menu.col),
479 MenuItem::Custom { id, .. } => {
480 MenuDispatch::Custom(id.clone(), menu.request.clone())
481 }
482 MenuItem::Separator => unreachable!(),
483 };
484
485 let state_click = state.clone();
486 let state_hover = state.clone();
487 let mut row = div()
488 .h(px(menu::MENU_ITEM_HEIGHT))
489 .px(px(menu::MENU_PADDING_X))
490 .flex()
491 .items_center()
492 .text_color(theme.menu_fg)
493 .text_size(px(menu::MENU_FONT_SIZE))
494 .child(label)
495 .on_mouse_move(move |_e: &MouseMoveEvent, _window, cx| {
496 state_hover.update(cx, |s, cx| {
497 if let Some(m) = s.context_menu.as_mut() {
498 if m.hovered != Some(this_idx) {
499 m.hovered = Some(this_idx);
500 cx.notify();
501 }
502 }
503 });
504 })
505 .on_mouse_down(
506 MouseButton::Left,
507 move |_e: &MouseDownEvent, _window, cx| {
508 state_click.update(cx, |s, cx| {
509 match &action {
510 MenuDispatch::Builtin(a, col) => {
511 s.pending_action = Some((*a, *col));
512 }
513 MenuDispatch::Custom(id, request) => {
514 if let Some(request) = request {
515 s.pending_custom_context_menu_action =
516 Some(PendingCustomContextMenuAction {
517 id: id.clone(),
518 request: request.clone(),
519 });
520 }
521 }
522 }
523 s.context_menu = None;
524 cx.notify();
525 });
526 },
527 );
528 if hovered {
529 row = row.bg(theme.menu_hover_bg);
530 }
531 rows.push(row.into_any_element());
532 }
533 }
534 }
535
536 let menu_body = div()
537 .flex()
538 .flex_col()
539 .w(px(menu_w))
540 .py(px(menu::MENU_INNER_PAD))
541 .bg(theme.menu_bg)
542 .border_1()
543 .border_color(theme.grid_line)
544 .children(rows);
545
546 let state_backdrop = state.clone();
549 let overlay = deferred(anchored().position(point(px(abs_x), px(abs_y))).child(
550 div().occlude().child(menu_body).on_mouse_down_out(
551 move |_e: &MouseDownEvent, _window, cx| {
552 state_backdrop.update(cx, |s, cx| {
553 if s.context_menu.is_some() {
554 s.context_menu = None;
555 s.filter_panel = None;
556 cx.notify();
557 }
558 });
559 },
560 ),
561 ))
562 .with_priority(CONTEXT_MENU_PRIORITY);
563
564 Some(overlay)
565}
566
567const FILTER_PANEL_WIDTH: f32 = 300.0;
569const FILTER_PANEL_MAX_ROWS: usize = 200;
571
572#[allow(clippy::too_many_lines)]
577fn render_filter_panel_overlay(
578 state: &Entity<GridState>,
579 cx: &mut Context<SqllyDataTable>,
580) -> Option<impl IntoElement> {
581 let s = state.read(cx);
582 let panel = s.filter_panel.clone()?;
583 let theme = s.theme.clone();
584 let col = panel.col;
585 let current_sort = s.sort;
586 let filter_active = s.filters.get(col).is_some_and(|f| f.is_active());
587 let grid_ox = f32::from(s.bounds.origin.x);
588 let grid_oy = f32::from(s.bounds.origin.y);
589
590 let abs_x = grid_ox + f32::from(panel.anchor.x);
595 let abs_y = grid_oy + f32::from(panel.anchor.y);
596
597 let c_bg = theme.menu_bg;
599 let c_line = theme.grid_line;
600 let c_fg = theme.menu_fg;
601 let c_accent = theme.sort_indicator;
602 let c_hover = theme.menu_hover_bg;
603 let c_muted = theme.muted_text;
604
605 let checkbox = move |checked: bool| {
606 let mut b = div()
607 .w(px(14.0))
608 .h(px(14.0))
609 .border_1()
610 .border_color(c_line)
611 .bg(c_bg)
612 .flex()
613 .items_center()
614 .justify_center();
615 if checked {
616 b = b.child(div().w(px(8.0)).h(px(8.0)).bg(c_accent));
617 }
618 b
619 };
620
621 let (asc_active, desc_active) = match current_sort {
623 Some((c, SortDirection::Ascending)) if c == col => (true, false),
624 Some((c, SortDirection::Descending)) if c == col => (false, true),
625 _ => (false, false),
626 };
627 let st_asc = state.clone();
628 let st_desc = state.clone();
629 let sort_row = div()
630 .flex()
631 .gap(px(6.0))
632 .child(
633 div()
634 .flex_1()
635 .h(px(26.0))
636 .flex()
637 .items_center()
638 .justify_center()
639 .border_1()
640 .border_color(c_line)
641 .bg(if asc_active { c_accent } else { c_hover })
642 .cursor_pointer()
643 .child("Ascending")
644 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
645 st_asc.update(cx, |s, cx| {
646 s.set_panel_sort(SortDirection::Ascending);
647 cx.notify();
648 });
649 }),
650 )
651 .child(
652 div()
653 .flex_1()
654 .h(px(26.0))
655 .flex()
656 .items_center()
657 .justify_center()
658 .border_1()
659 .border_color(c_line)
660 .bg(if desc_active { c_accent } else { c_hover })
661 .cursor_pointer()
662 .child("Descending")
663 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
664 st_desc.update(cx, |s, cx| {
665 s.set_panel_sort(SortDirection::Descending);
666 cx.notify();
667 });
668 }),
669 );
670
671 let st_op_toggle = state.clone();
673 let op_button = div()
674 .h(px(26.0))
675 .px(px(8.0))
676 .flex()
677 .items_center()
678 .border_1()
679 .border_color(c_line)
680 .bg(c_bg)
681 .cursor_pointer()
682 .child(panel.current_op_label())
683 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
684 st_op_toggle.update(cx, |s, cx| {
685 s.toggle_filter_op_menu();
686 cx.notify();
687 });
688 });
689
690 let op_menu = panel.op_menu_open.then(|| {
691 let mut items: Vec<gpui::AnyElement> = Vec::new();
692 for (i, label) in panel.op_labels().iter().enumerate() {
693 let selected = i == panel.op_index;
694 let st_pick = state.clone();
695 items.push(
696 div()
697 .h(px(24.0))
698 .px(px(8.0))
699 .flex()
700 .items_center()
701 .bg(if selected { c_accent } else { c_bg })
702 .cursor_pointer()
703 .child(*label)
704 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
705 st_pick.update(cx, |s, cx| {
706 s.set_filter_operator(i);
707 cx.notify();
708 });
709 })
710 .into_any_element(),
711 );
712 }
713 div()
714 .flex()
715 .flex_col()
716 .border_1()
717 .border_color(c_line)
718 .bg(c_bg)
719 .children(items)
720 });
721
722 let operand_field = |value: &str, focused: bool, placeholder: &str, input: FilterInput| {
724 let st_focus = state.clone();
725 let (text, is_placeholder) = if value.is_empty() {
726 (placeholder.to_owned(), true)
727 } else {
728 (value.to_owned(), false)
729 };
730 div()
731 .h(px(26.0))
732 .px(px(6.0))
733 .flex()
734 .items_center()
735 .gap(px(2.0))
736 .border_1()
737 .border_color(if focused { c_accent } else { c_line })
738 .bg(c_bg)
739 .cursor_pointer()
740 .child(
741 div()
742 .text_color(if is_placeholder { c_muted } else { c_fg })
743 .child(text),
744 )
745 .children(focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
746 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
747 st_focus.update(cx, |s, cx| {
748 s.set_filter_focus(input);
749 cx.notify();
750 });
751 })
752 };
753
754 let operand_placeholder = if panel.kind == crate::data::ColumnKind::Date {
755 "YYYY-MM-DD"
756 } else if crate::filter::uses_number_ops(panel.kind) {
757 "value"
758 } else if panel.op_index == 7 {
759 "regex"
761 } else {
762 "value"
763 };
764 let operands = panel.needs_operand().then(|| {
765 let mut row = div().flex().flex_col().gap(px(4.0)).child(operand_field(
766 &panel.operand_a.value,
767 panel.focus == FilterInput::OperandA,
768 operand_placeholder,
769 FilterInput::OperandA,
770 ));
771 if panel.needs_second_operand() {
772 row = row
773 .child(div().text_color(c_muted).text_size(px(11.0)).child("and"))
774 .child(operand_field(
775 &panel.operand_b.value,
776 panel.focus == FilterInput::OperandB,
777 operand_placeholder,
778 FilterInput::OperandB,
779 ));
780 }
781 row
782 });
783
784 let st_search = state.clone();
786 let search_focused = panel.focus == FilterInput::Search;
787 let (search_text, search_is_ph) = if panel.search.value.is_empty() {
788 ("Search".to_owned(), true)
789 } else {
790 (panel.search.value.clone(), false)
791 };
792 let search_box = div()
793 .h(px(26.0))
794 .px(px(6.0))
795 .flex()
796 .items_center()
797 .gap(px(2.0))
798 .border_1()
799 .border_color(if search_focused { c_accent } else { c_line })
800 .bg(c_bg)
801 .cursor_pointer()
802 .child(
803 div()
804 .text_color(if search_is_ph { c_muted } else { c_fg })
805 .child(search_text),
806 )
807 .children(search_focused.then(|| div().w(px(1.0)).h(px(14.0)).bg(c_accent)))
808 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
809 st_search.update(cx, |s, cx| {
810 s.set_filter_focus(FilterInput::Search);
811 cx.notify();
812 });
813 });
814
815 let st_all = state.clone();
817 let select_all_row = div()
818 .h(px(24.0))
819 .flex()
820 .items_center()
821 .gap(px(6.0))
822 .cursor_pointer()
823 .child(checkbox(panel.all_checked()))
824 .child("(Select All)")
825 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
826 st_all.update(cx, |s, cx| {
827 s.toggle_filter_select_all();
828 cx.notify();
829 });
830 });
831
832 let visible = panel.visible_indices();
833 let mut value_rows: Vec<gpui::AnyElement> = Vec::new();
834 for &idx in visible.iter().take(FILTER_PANEL_MAX_ROWS) {
835 let row = &panel.distinct[idx];
836 let st_val = state.clone();
837 value_rows.push(
838 div()
839 .h(px(22.0))
840 .flex()
841 .items_center()
842 .gap(px(6.0))
843 .cursor_pointer()
844 .child(checkbox(row.checked))
845 .child(div().text_color(c_fg).child(row.label.clone()))
846 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
847 st_val.update(cx, |s, cx| {
848 s.toggle_filter_value(idx);
849 cx.notify();
850 });
851 })
852 .into_any_element(),
853 );
854 }
855 let truncated = visible.len() > FILTER_PANEL_MAX_ROWS;
856 let value_list = div()
857 .id("filter-value-list")
858 .flex()
859 .flex_col()
860 .max_h(px(180.0))
861 .overflow_y_scroll()
862 .children(value_rows)
863 .children(truncated.then(|| {
864 div()
865 .text_color(c_muted)
866 .text_size(px(11.0))
867 .child("Refine search to see more…")
868 }));
869
870 let st_clear = state.clone();
872 let st_close = state.clone();
873 let clear_bg = if filter_active { c_hover } else { c_bg };
874 let clear_fg = if filter_active { c_fg } else { c_muted };
875 let clear_border = if filter_active { c_line } else { c_muted };
876 let buttons_row = div()
877 .flex()
878 .gap(px(6.0))
879 .child(
880 div()
881 .flex_1()
882 .h(px(28.0))
883 .flex()
884 .items_center()
885 .justify_center()
886 .border_1()
887 .border_color(clear_border)
888 .bg(clear_bg)
889 .text_color(clear_fg)
890 .cursor_pointer()
891 .child("Clear Filter")
892 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
893 if !filter_active {
894 return;
895 }
896 st_clear.update(cx, |s, cx| {
897 s.clear_filter_panel();
898 cx.notify();
899 });
900 }),
901 )
902 .child(
903 div()
904 .flex_1()
905 .h(px(28.0))
906 .flex()
907 .items_center()
908 .justify_center()
909 .border_1()
910 .border_color(c_line)
911 .bg(c_hover)
912 .cursor_pointer()
913 .child("Close")
914 .on_mouse_down(MouseButton::Left, move |_e: &MouseDownEvent, _w, cx| {
915 st_close.update(cx, |s, cx| {
916 s.filter_panel = None;
917 cx.notify();
918 });
919 }),
920 );
921
922 let panel_body = div()
923 .flex()
924 .flex_col()
925 .w(px(FILTER_PANEL_WIDTH))
926 .p(px(10.0))
927 .gap(px(8.0))
928 .bg(c_bg)
929 .border_1()
930 .border_color(c_line)
931 .text_color(c_fg)
932 .text_size(px(13.0))
933 .child(div().text_color(c_muted).text_size(px(11.0)).child("Sort"))
934 .child(sort_row)
935 .child(
936 div()
937 .text_color(c_muted)
938 .text_size(px(11.0))
939 .child("Filter"),
940 )
941 .child(op_button)
942 .children(op_menu)
943 .children(operands)
944 .child(search_box)
945 .child(select_all_row)
946 .child(value_list)
947 .child(buttons_row);
948
949 let st_backdrop = state.clone();
950 let overlay = deferred(
951 anchored()
952 .anchor(Corner::BottomLeft)
953 .position(point(px(abs_x), px(abs_y)))
954 .child(div().occlude().child(panel_body).on_mouse_down_out(
955 move |_e: &MouseDownEvent, _window, cx| {
956 st_backdrop.update(cx, |s, cx| {
957 if s.filter_panel.is_some() {
958 s.filter_panel = None;
959 cx.notify();
960 }
961 });
962 },
963 )),
964 )
965 .with_priority(CONTEXT_MENU_PRIORITY);
966
967 Some(overlay)
968}
969
970fn render_busy_overlay(
976 state: &Entity<GridState>,
977 cx: &mut Context<SqllyDataTable>,
978) -> Option<impl IntoElement> {
979 let s = state.read(cx);
980 let busy = s.busy.clone()?;
981 let theme = s.theme.clone();
982 let track = theme.grid_line;
983 let accent = theme.sort_indicator;
984
985 let bar: gpui::AnyElement = if let Some(p) = busy.progress {
986 let p = p.clamp(0.0, 1.0);
987 div()
988 .h_full()
989 .w(relative(p))
990 .rounded(px(3.0))
991 .bg(accent)
992 .into_any_element()
993 } else {
994 div()
995 .h_full()
996 .w(relative(0.3))
997 .rounded(px(3.0))
998 .bg(accent)
999 .with_animation(
1000 "busy-indeterminate",
1001 Animation::new(std::time::Duration::from_millis(900))
1002 .repeat()
1003 .with_easing(pulsating_between(0.15, 0.85)),
1004 |el, delta| el.w(relative(delta)),
1005 )
1006 .into_any_element()
1007 };
1008
1009 let card = div()
1010 .flex()
1011 .flex_col()
1012 .gap(px(10.0))
1013 .p(px(16.0))
1014 .min_w(px(220.0))
1015 .rounded(px(8.0))
1016 .bg(theme.menu_bg)
1017 .border_1()
1018 .border_color(theme.grid_line)
1019 .child(
1020 div()
1021 .text_color(theme.menu_fg)
1022 .text_size(px(14.0))
1023 .child(busy.label.clone()),
1024 )
1025 .child(
1026 div()
1027 .w_full()
1028 .h(px(6.0))
1029 .rounded(px(3.0))
1030 .bg(track)
1031 .child(bar),
1032 );
1033
1034 let overlay = div()
1035 .absolute()
1036 .top_0()
1037 .left_0()
1038 .size_full()
1039 .occlude()
1040 .flex()
1041 .items_center()
1042 .justify_center()
1043 .bg(hsla(0.0, 0.0, 0.0, 0.35))
1044 .child(card);
1045
1046 Some(overlay)
1047}
1048
1049enum MenuDispatch {
1052 Builtin(menu::MenuAction, usize),
1053 Custom(
1054 String,
1055 Option<crate::grid::context_menu::ContextMenuRequest>,
1056 ),
1057}