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