1use crate::config::GridConfig;
7use crate::data::GridData;
8use crate::grid::paint::{paint_grid, paint_status_bar, PaintData, StatusBarData};
9use crate::grid::state::state_inner;
10use crate::grid::state::{GridState, EDGE_SCROLL_TICK_MS};
11use crate::grid::theme::GridTheme;
12use crate::grid::{menu, HitResult, MenuItem};
13
14use gpui::{
15 canvas, div, point, px, App, AppContext, Context, Entity, FocusHandle, Focusable,
16 InteractiveElement, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
17 ParentElement, Render, ScrollWheelEvent, Styled, Window,
18};
19
20pub struct SqllyDataTable {
22 pub state: Entity<GridState>,
23}
24
25impl SqllyDataTable {
26 #[must_use]
28 pub fn new(state: Entity<GridState>) -> Self {
29 Self { state }
30 }
31
32 #[must_use]
34 pub fn builder(data: GridData) -> SqllyDataTableBuilder {
35 SqllyDataTableBuilder {
36 data,
37 config: GridConfig::default(),
38 }
39 }
40}
41
42pub struct SqllyDataTableBuilder {
44 data: GridData,
45 config: GridConfig,
46}
47
48impl SqllyDataTableBuilder {
49 #[must_use]
51 pub fn config(mut self, config: GridConfig) -> Self {
52 self.config = config;
53 self
54 }
55
56 #[must_use]
58 pub fn theme(self, theme: GridTheme) -> Self {
59 let _ = theme;
60 self
61 }
62
63 pub fn build(self, cx: &mut App) -> SqllyDataTable {
65 let focus = cx.focus_handle();
66 let state = cx.new(|_cx| GridState::new(self.data, self.config, focus.clone()));
67 SqllyDataTable { state }
68 }
69}
70
71impl Focusable for SqllyDataTable {
72 fn focus_handle(&self, cx: &App) -> FocusHandle {
73 self.state.read(cx).focus_handle.clone()
74 }
75}
76
77impl Render for SqllyDataTable {
78 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
79 let state_canvas = self.state.clone();
80 let state_status = self.state.clone();
81 let state_mouse = self.state.clone();
82 let state_move = self.state.clone();
83 let state_up = self.state.clone();
84 let state_scroll = self.state.clone();
85 let state_key = self.state.clone();
86 let state_right = self.state.clone();
87 let bg = self.state.read(cx).theme.bg;
88 let _focus_handle = self.state.read(cx).focus_handle.clone();
89 let status_h = self.state.read(cx).status_bar_height;
90
91 if let Some((action, col)) = self.state.read(cx).pending_action {
94 self.state.update(cx, |s, cx| {
95 s.execute_action(action, col, cx);
96 s.pending_action = None;
97 });
98 }
99
100 if self.state.read(cx).is_dragging {
104 let state_edge = self.state.clone();
105 cx.spawn(async move |_weak, cx| loop {
106 gpui::Timer::after(std::time::Duration::from_millis(EDGE_SCROLL_TICK_MS)).await;
107 let res = cx.update(|cx| state_edge.update(cx, |s, _cx| s.apply_edge_scroll()));
108 if let Ok(true) = res {
109 let _ = state_edge.update(cx, |_s, cx| cx.notify());
110 }
111 let dragging_res = cx.update(|cx| state_edge.read(cx).is_dragging);
112 if !matches!(dragging_res, Ok(true)) {
113 break;
114 }
115 })
116 .detach();
117 }
118
119 div()
120 .flex()
121 .flex_col()
122 .size_full()
123 .bg(bg)
124 .child(
125 canvas(
126 move |bounds, _window, cx| -> PaintData {
127 state_canvas.update(cx, |s, cx| {
128 if s.bounds != bounds {
129 s.bounds = bounds;
130 cx.notify();
131 }
132 });
133 let s = state_canvas.read(cx);
134 PaintData::from_state(s)
135 },
136 move |bounds, data, window, cx| {
137 paint_grid(&data, window, cx, bounds);
138 },
139 )
140 .flex_1(),
141 )
142 .child(
143 canvas(
144 move |_bounds, _window, cx| -> StatusBarData {
145 let s = state_status.read(cx);
146 StatusBarData::from_state(s)
147 },
148 move |bounds, data, window, cx| {
149 paint_status_bar(&data, window, cx, bounds);
150 },
151 )
152 .h(px(status_h)),
153 )
154 .on_mouse_down(
155 MouseButton::Left,
156 move |event: &MouseDownEvent, _window, cx| {
157 state_mouse.update(cx, |s, cx| {
158 if let Some(menu) = s.context_menu.clone() {
159 let cw = s.char_width;
160 let (mx_rel, my_rel) = state_inner::screen_to_content(
161 event.position,
162 s.bounds.origin,
163 s.scroll_handle.offset(),
164 );
165 let w = menu.width_for(cw);
166 let total_h = menu.total_height();
167 let ax = f32::from(menu.anchor.x);
168 let ay = f32::from(menu.anchor.y);
169 if mx_rel >= ax
170 && mx_rel <= ax + w
171 && my_rel >= ay
172 && my_rel <= ay + total_h
173 {
174 if let Some(action_idx) = menu::hover_at(&menu, mx_rel, my_rel, cw)
175 {
176 let mut cur = 0;
177 for item in &menu.items {
178 if let MenuItem::Action(a) = item {
179 if cur == action_idx {
180 s.pending_action = Some((*a, menu.col));
181 s.context_menu = None;
182 cx.notify();
183 return;
184 }
185 cur += 1;
186 }
187 }
188 }
189 } else {
190 s.context_menu = None;
191 s.filter_prompt = None;
192 }
193 }
194 s.handle_mouse_down(event.position, event.modifiers.shift);
195 cx.notify();
196 });
197 },
198 )
199 .on_mouse_down(
200 MouseButton::Right,
201 move |event: &MouseDownEvent, _window, cx| {
202 state_right.update(cx, |s, cx| {
203 let pos = event.position;
204 let hit = s.hit_test(pos);
205 match hit {
206 HitResult::ColumnHeader(col) | HitResult::SortButton(col) => {
207 s.open_context_menu(col, pos);
208 }
209 _ => {
210 s.context_menu = None;
211 s.filter_prompt = None;
212 }
213 }
214 cx.notify();
215 });
216 },
217 )
218 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
219 state_move.update(cx, |s, cx| {
220 s.handle_mouse_move(event.position, event.pressed_button);
221 cx.notify();
222 });
223 })
224 .on_mouse_up(
225 MouseButton::Left,
226 move |_event: &MouseUpEvent, _window, cx| {
227 state_up.update(cx, |s, cx| {
228 s.handle_mouse_up();
229 cx.notify();
230 });
231 },
232 )
233 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
234 state_scroll.update(cx, |s, cx| {
235 let line_h = px(s.row_height);
236 let delta = event.delta.pixel_delta(line_h);
237 let scroll = s.scroll_handle.offset();
238 let (mx, my) = s.max_scroll();
239 let new_y = (f32::from(scroll.y) - f32::from(delta.y)).clamp(0.0, my);
240 let new_x = (f32::from(scroll.x) - f32::from(delta.x)).clamp(0.0, mx);
241 s.scroll_handle.set_offset(point(px(new_x), px(new_y)));
242 if s.drag_start.is_some() {
243 s.handle_scroll_drag();
244 }
245 cx.notify();
246 });
247 })
248 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
249 let ks = &event.keystroke;
250 if ks.modifiers.platform && ks.key == "q" {
251 cx.quit();
252 return;
253 }
254 state_key.update(cx, |s, cx| {
255 let kb = &s.config.key_bindings;
256 if kb.select_all.matches(ks) {
257 s.select_all();
258 } else if kb.copy.matches(ks) {
259 s.copy_selection(false, cx);
260 } else if kb.copy_with_headers.matches(ks) {
261 s.copy_selection(true, cx);
262 } else if kb.page_up.matches(ks) {
263 s.page_up();
264 } else if kb.page_down.matches(ks) {
265 s.page_down();
266 } else {
267 s.handle_key(ks);
268 }
269 cx.notify();
270 });
271 })
272 }
273}