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 status_h = self.state.read(cx).status_bar_height;
112
113 if let Some((action, col)) = self.state.read(cx).pending_action {
116 self.state.update(cx, |s, cx| {
117 s.execute_action(action, col, cx);
118 s.pending_action = None;
119 });
120 }
121
122 if let Some(pending) = self
124 .state
125 .read(cx)
126 .pending_custom_context_menu_action
127 .clone()
128 {
129 self.state.update(cx, |s, cx| {
130 s.pending_custom_context_menu_action = None;
131 s.execute_custom_context_menu_action(pending, cx);
132 });
133 }
134
135 if self.state.read(cx).is_dragging {
139 let state_edge = self.state.clone();
140 cx.spawn(async move |_weak, cx| loop {
141 gpui::Timer::after(std::time::Duration::from_millis(EDGE_SCROLL_TICK_MS)).await;
142 let res = cx.update(|cx| state_edge.update(cx, |s, _cx| s.apply_edge_scroll()));
143 if let Ok(true) = res {
144 let _ = state_edge.update(cx, |_s, cx| cx.notify());
145 }
146 let dragging_res = cx.update(|cx| state_edge.read(cx).is_dragging);
147 if !matches!(dragging_res, Ok(true)) {
148 break;
149 }
150 })
151 .detach();
152 }
153
154 div()
155 .flex()
156 .flex_col()
157 .size_full()
158 .bg(bg)
159 .child(
160 canvas(
161 move |bounds, _window, cx| -> PaintData {
162 state_canvas.update(cx, |s, cx| {
163 if s.bounds != bounds {
164 s.bounds = bounds;
165 cx.notify();
166 }
167 });
168 let s = state_canvas.read(cx);
169 PaintData::from_state(s)
170 },
171 move |bounds, data, window, cx| {
172 paint_grid(&data, window, cx, bounds);
173 },
174 )
175 .flex_1(),
176 )
177 .child(
178 canvas(
179 move |_bounds, _window, cx| -> StatusBarData {
180 let s = state_status.read(cx);
181 StatusBarData::from_state(s)
182 },
183 move |bounds, data, window, cx| {
184 paint_status_bar(&data, window, cx, bounds);
185 },
186 )
187 .h(px(status_h)),
188 )
189 .on_mouse_down(
190 MouseButton::Left,
191 move |event: &MouseDownEvent, _window, cx| {
192 state_mouse.update(cx, |s, cx| {
193 if let Some(menu) = s.context_menu.clone() {
194 let cw = s.char_width;
195 let (mx_rel, my_rel) = state_inner::screen_to_content(
196 event.position,
197 s.bounds.origin,
198 s.scroll_handle.offset(),
199 );
200 let w = menu.width_for(cw);
201 let total_h = menu.total_height();
202 let ax = f32::from(menu.anchor.x);
203 let ay = f32::from(menu.anchor.y);
204 if mx_rel >= ax
205 && mx_rel <= ax + w
206 && my_rel >= ay
207 && my_rel <= ay + total_h
208 {
209 if let Some(action_idx) = menu::hover_at(&menu, mx_rel, my_rel, cw)
210 {
211 let mut cur = 0;
212 for item in &menu.items {
213 match item {
214 MenuItem::Action(a) => {
215 if cur == action_idx {
216 s.pending_action = Some((*a, menu.col));
217 s.context_menu = None;
218 cx.notify();
219 return;
220 }
221 cur += 1;
222 }
223 MenuItem::Custom { id, .. } => {
224 if cur == action_idx {
225 if let Some(request) = &menu.request {
226 s.pending_custom_context_menu_action =
227 Some(PendingCustomContextMenuAction {
228 id: id.clone(),
229 request: request.clone(),
230 });
231 }
232 s.context_menu = None;
233 cx.notify();
234 return;
235 }
236 cur += 1;
237 }
238 MenuItem::Separator => {}
239 }
240 }
241 }
242 } else {
243 s.context_menu = None;
244 s.filter_prompt = None;
245 }
246 }
247 s.handle_mouse_down(event.position, event.modifiers.shift);
248 cx.notify();
249 });
250 },
251 )
252 .on_mouse_down(
253 MouseButton::Right,
254 move |event: &MouseDownEvent, _window, cx| {
255 state_right.update(cx, |s, cx| {
256 let pos = event.position;
257 let hit = s.hit_test(pos);
258
259 if s.context_menu_provider.is_none() {
261 match hit {
262 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
263 s.open_context_menu(col, pos);
264 }
265 _ => {
266 s.context_menu = None;
267 s.filter_prompt = None;
268 }
269 }
270 cx.notify();
271 return;
272 }
273
274 let Some(target) = s.context_menu_target_from_hit(hit) else {
276 s.context_menu = None;
277 s.filter_prompt = None;
278 cx.notify();
279 return;
280 };
281
282 let effective = s.effective_selection_for_context_target(&target);
283 if effective != s.selection {
284 s.selection = effective.clone();
285 }
286
287 let request = s.build_context_menu_request(target, &effective);
288 let col = request.target.column_index().unwrap_or(0);
289
290 let Some(provider) = s.context_menu_provider.clone() else {
291 return;
292 };
293 let public_items = provider.menu_items(&request);
294 let items = GridState::convert_context_menu_items(public_items);
295
296 if items.is_empty() {
297 s.context_menu = None;
298 } else {
299 s.context_menu =
300 Some(menu::ContextMenu::custom(col, pos, items, request));
301 }
302 s.filter_prompt = None;
303 cx.notify();
304 });
305 },
306 )
307 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
308 state_move.update(cx, |s, cx| {
309 s.handle_mouse_move(event.position, event.pressed_button);
310 cx.notify();
311 });
312 })
313 .on_mouse_up(
314 MouseButton::Left,
315 move |_event: &MouseUpEvent, _window, cx| {
316 state_up.update(cx, |s, cx| {
317 s.handle_mouse_up();
318 cx.notify();
319 });
320 },
321 )
322 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
323 state_scroll.update(cx, |s, cx| {
324 let line_h = px(s.row_height);
325 let delta = event.delta.pixel_delta(line_h);
326 let scroll = s.scroll_handle.offset();
327 let (mx, my) = s.max_scroll();
328 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
329 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
330 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
331 if s.drag_start.is_some() {
332 s.handle_scroll_drag();
333 }
334 cx.notify();
335 });
336 })
337 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
338 let ks = &event.keystroke;
339 if ks.modifiers.platform && ks.key == "q" {
340 cx.quit();
341 return;
342 }
343 state_key.update(cx, |s, cx| {
344 let kb = &s.config.key_bindings;
345 if kb.select_all.matches(ks) {
346 s.select_all();
347 } else if kb.copy.matches(ks) {
348 s.copy_selection(false, cx);
349 } else if kb.copy_with_headers.matches(ks) {
350 s.copy_selection(true, cx);
351 } else if kb.page_up.matches(ks) {
352 s.page_up();
353 } else if kb.page_down.matches(ks) {
354 s.page_down();
355 } else {
356 s.handle_key(ks);
357 }
358 cx.notify();
359 });
360 })
361 }
362}