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::{GridState, EDGE_SCROLL_TICK_MS};
14use crate::grid::theme::GridTheme;
15use crate::grid::{menu, HitResult, MenuItem};
16
17use gpui::{
18 canvas, div, point, px, App, AppContext, Context, Entity, FocusHandle, Focusable,
19 InteractiveElement, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
20 ParentElement, Render, ScrollWheelEvent, Styled, Window,
21};
22
23pub struct SqllyDataTable {
25 pub state: Entity<GridState>,
26}
27
28impl SqllyDataTable {
29 #[must_use]
31 pub fn new(state: Entity<GridState>) -> Self {
32 Self { state }
33 }
34
35 #[must_use]
37 pub fn builder(data: GridData) -> SqllyDataTableBuilder {
38 SqllyDataTableBuilder {
39 data,
40 config: GridConfig::default(),
41 context_menu_provider: None,
42 }
43 }
44}
45
46pub struct SqllyDataTableBuilder {
48 data: GridData,
49 config: GridConfig,
50 context_menu_provider: Option<ContextMenuProviderHandle>,
51}
52
53impl SqllyDataTableBuilder {
54 #[must_use]
56 pub fn config(mut self, config: GridConfig) -> Self {
57 self.config = config;
58 self
59 }
60
61 #[must_use]
63 pub fn theme(self, theme: GridTheme) -> Self {
64 let _ = theme;
65 self
66 }
67
68 #[must_use]
75 pub fn context_menu_provider(mut self, provider: impl ContextMenuProvider + 'static) -> Self {
76 self.context_menu_provider = Some(ContextMenuProviderHandle::new(provider));
77 self
78 }
79
80 pub fn build(self, cx: &mut App) -> SqllyDataTable {
82 let focus = cx.focus_handle();
83 let provider = self.context_menu_provider;
84 let state = cx.new(|_cx| {
85 let mut s = GridState::new(self.data, self.config, focus.clone());
86 s.context_menu_provider = provider;
87 s
88 });
89 SqllyDataTable { state }
90 }
91}
92
93impl Focusable for SqllyDataTable {
94 fn focus_handle(&self, cx: &App) -> FocusHandle {
95 self.state.read(cx).focus_handle.clone()
96 }
97}
98
99impl Render for SqllyDataTable {
100 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
101 let state_canvas = self.state.clone();
102 let state_status = self.state.clone();
103 let state_mouse = self.state.clone();
104 let state_move = self.state.clone();
105 let state_up = self.state.clone();
106 let state_scroll = self.state.clone();
107 let state_key = self.state.clone();
108 let state_right = self.state.clone();
109 let bg = self.state.read(cx).theme.bg;
110 let focus_handle = self.state.read(cx).focus_handle.clone();
111 let focus_left = focus_handle.clone();
112 let focus_right = focus_handle.clone();
113 let status_h = self.state.read(cx).status_bar_height;
114
115 if let Some((action, col)) = self.state.read(cx).pending_action {
118 self.state.update(cx, |s, cx| {
119 s.execute_action(action, col, cx);
120 s.pending_action = None;
121 });
122 }
123
124 if let Some(pending) = self
126 .state
127 .read(cx)
128 .pending_custom_context_menu_action
129 .clone()
130 {
131 self.state.update(cx, |s, cx| {
132 s.pending_custom_context_menu_action = None;
133 s.execute_custom_context_menu_action(pending, cx);
134 });
135 }
136
137 if self.state.read(cx).is_dragging {
141 let state_edge = self.state.clone();
142 cx.spawn(async move |_weak, cx| loop {
143 gpui::Timer::after(std::time::Duration::from_millis(EDGE_SCROLL_TICK_MS)).await;
144 let res = cx.update(|cx| state_edge.update(cx, |s, _cx| s.apply_edge_scroll()));
145 if let Ok(true) = res {
146 let _ = state_edge.update(cx, |_s, cx| cx.notify());
147 }
148 let dragging_res = cx.update(|cx| state_edge.read(cx).is_dragging);
149 if !matches!(dragging_res, Ok(true)) {
150 break;
151 }
152 })
153 .detach();
154 }
155
156 div()
157 .flex()
158 .flex_col()
159 .size_full()
160 .track_focus(&focus_handle)
161 .bg(bg)
162 .child(
163 canvas(
164 move |bounds, _window, cx| -> PaintData {
165 state_canvas.update(cx, |s, cx| {
166 if s.bounds != bounds {
167 s.bounds = bounds;
168 cx.notify();
169 }
170 });
171 let s = state_canvas.read(cx);
172 PaintData::from_state(s)
173 },
174 move |bounds, data, window, cx| {
175 paint_grid(&data, window, cx, bounds);
176 },
177 )
178 .flex_1(),
179 )
180 .child(
181 canvas(
182 move |_bounds, _window, cx| -> StatusBarData {
183 let s = state_status.read(cx);
184 StatusBarData::from_state(s)
185 },
186 move |bounds, data, window, cx| {
187 paint_status_bar(&data, window, cx, bounds);
188 },
189 )
190 .h(px(status_h)),
191 )
192 .on_mouse_down(
193 MouseButton::Left,
194 move |event: &MouseDownEvent, window, cx| {
195 window.focus(&focus_left);
196 state_mouse.update(cx, |s, cx| {
197 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
202 if let Some(menu) = s.context_menu.clone() {
203 let cw = s.char_width;
204 let mx_rel = f32::from(rel.x);
208 let my_rel = f32::from(rel.y);
209 let w = menu.width_for(cw);
210 let total_h = menu.total_height();
211 let ax = f32::from(menu.anchor.x);
212 let ay = f32::from(menu.anchor.y);
213 if mx_rel >= ax
214 && mx_rel <= ax + w
215 && my_rel >= ay
216 && my_rel <= ay + total_h
217 {
218 if let Some(action_idx) = menu::hover_at(&menu, mx_rel, my_rel, cw)
219 {
220 let mut cur = 0;
221 for item in &menu.items {
222 match item {
223 MenuItem::Action(a) => {
224 if cur == action_idx {
225 s.pending_action = Some((*a, menu.col));
226 s.context_menu = None;
227 cx.notify();
228 return;
229 }
230 cur += 1;
231 }
232 MenuItem::Custom { id, .. } => {
233 if cur == action_idx {
234 if let Some(request) = &menu.request {
235 s.pending_custom_context_menu_action =
236 Some(PendingCustomContextMenuAction {
237 id: id.clone(),
238 request: request.clone(),
239 });
240 }
241 s.context_menu = None;
242 cx.notify();
243 return;
244 }
245 cur += 1;
246 }
247 MenuItem::Separator => {}
248 }
249 }
250 }
251 } else {
252 s.context_menu = None;
253 s.filter_prompt = None;
254 }
255 }
256 s.handle_mouse_down(rel, event.modifiers.shift);
257 cx.notify();
258 });
259 },
260 )
261 .on_mouse_down(
262 MouseButton::Right,
263 move |event: &MouseDownEvent, window, cx| {
264 window.focus(&focus_right);
265 state_right.update(cx, |s, cx| {
266 let pos = state_inner::to_grid_relative(event.position, s.bounds.origin);
267 let hit = s.hit_test(pos);
268
269 if s.context_menu_provider.is_none() {
271 match hit {
272 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
273 s.open_context_menu(col, pos);
274 }
275 _ => {
276 s.context_menu = None;
277 s.filter_prompt = None;
278 }
279 }
280 cx.notify();
281 return;
282 }
283
284 let Some(target) = s.context_menu_target_from_hit(hit) else {
286 s.context_menu = None;
287 s.filter_prompt = None;
288 cx.notify();
289 return;
290 };
291
292 let effective = s.effective_selection_for_context_target(&target);
293 if effective != s.selection {
294 s.selection = effective.clone();
295 }
296
297 let request = s.build_context_menu_request(target, &effective);
298 let col = request.target.column_index().unwrap_or(0);
299
300 let Some(provider) = s.context_menu_provider.clone() else {
301 return;
302 };
303 let public_items = provider.menu_items(&request);
304 let items = GridState::convert_context_menu_items(public_items);
305
306 if items.is_empty() {
307 s.context_menu = None;
308 } else {
309 s.context_menu =
310 Some(menu::ContextMenu::custom(col, pos, items, request));
311 }
312 s.filter_prompt = None;
313 cx.notify();
314 });
315 },
316 )
317 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
318 state_move.update(cx, |s, cx| {
319 let rel = state_inner::to_grid_relative(event.position, s.bounds.origin);
320 s.handle_mouse_move(rel, event.pressed_button);
321 cx.notify();
322 });
323 })
324 .on_mouse_up(
325 MouseButton::Left,
326 move |_event: &MouseUpEvent, _window, cx| {
327 state_up.update(cx, |s, cx| {
328 s.handle_mouse_up();
329 cx.notify();
330 });
331 },
332 )
333 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
334 state_scroll.update(cx, |s, cx| {
335 let line_h = px(s.row_height);
336 let delta = event.delta.pixel_delta(line_h);
337 let scroll = s.scroll_handle.offset();
338 let (mx, my) = s.max_scroll();
339 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
340 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
341 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
342 if s.drag_start.is_some() {
343 s.handle_scroll_drag();
344 }
345 cx.notify();
346 });
347 })
348 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
349 let ks = &event.keystroke;
350 if ks.modifiers.platform && ks.key == "q" {
351 cx.quit();
352 return;
353 }
354 state_key.update(cx, |s, cx| {
355 let kb = &s.config.key_bindings;
356 if kb.select_all.matches(ks) {
357 s.select_all();
358 } else if kb.copy.matches(ks) {
359 s.copy_selection(false, cx);
360 } else if kb.copy_with_headers.matches(ks) {
361 s.copy_selection(true, cx);
362 } else if kb.page_up.matches(ks) {
363 s.page_up();
364 } else if kb.page_down.matches(ks) {
365 s.page_down();
366 } else {
367 s.handle_key(ks);
368 }
369 cx.notify();
370 });
371 })
372 }
373}