1use crate::grid::menu::{MENU_FONT_SIZE, MENU_INNER_PAD, MENU_ITEM_HEIGHT, MENU_PADDING_X};
7use crate::grid::selection::to_grid_relative;
8use crate::pivot::context_menu::PivotMenuItem;
9use crate::pivot::paint::{paint_pivot_grid, PivotPaintData};
10use crate::pivot::state::{PivotHitResult, PivotState};
11
12use gpui::{
13 anchored, canvas, deferred, div, point, px, App, Context, Entity, FocusHandle, Focusable,
14 InteractiveElement, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
15 MouseUpEvent, ParentElement, Render, ScrollWheelEvent, Styled, Window,
16};
17
18const PIVOT_MENU_PRIORITY: usize = 1_000_000;
21
22pub struct PivotGrid {
24 pub state: Entity<PivotState>,
26}
27
28impl PivotGrid {
29 #[must_use]
31 pub fn new(state: Entity<PivotState>) -> Self {
32 Self { state }
33 }
34}
35
36impl Focusable for PivotGrid {
37 fn focus_handle(&self, cx: &App) -> FocusHandle {
38 self.state.read(cx).focus_handle.clone()
39 }
40}
41
42impl Render for PivotGrid {
43 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
44 let state_canvas = self.state.clone();
45 let state_down = self.state.clone();
46 let state_move = self.state.clone();
47 let state_up = self.state.clone();
48 let state_scroll = self.state.clone();
49 let state_key = self.state.clone();
50 let state_right = self.state.clone();
51 let bg = self.state.read(cx).theme.bg;
52 let focus_handle = self.state.read(cx).focus_handle.clone();
53 let focus_down = focus_handle.clone();
54 let focus_right = focus_handle.clone();
55
56 div()
57 .size_full()
58 .relative()
59 .track_focus(&focus_handle)
60 .bg(bg)
61 .child(
62 canvas(
63 move |bounds, window, cx| -> PivotPaintData {
64 let viewport = window.viewport_size();
65 state_canvas.update(cx, |s, cx| {
66 let mut dirty = false;
67 if s.bounds != bounds {
68 s.bounds = bounds;
69 s.clamp_scroll_to_bounds();
70 dirty = true;
71 }
72 if s.window_viewport != viewport {
73 s.window_viewport = viewport;
74 }
75 if dirty {
76 cx.notify();
77 }
78 });
79 let s = state_canvas.read(cx);
80 PivotPaintData::from_state(s)
81 },
82 move |bounds, data, window, cx| {
83 paint_pivot_grid(&data, window, cx, bounds);
84 },
85 )
86 .size_full(),
87 )
88 .on_mouse_down(
89 MouseButton::Left,
90 move |event: &MouseDownEvent, window, cx| {
91 window.focus(&focus_down);
92 state_down.update(cx, |s, cx| {
93 s.menu = None;
96 let rel = to_grid_relative(event.position, s.bounds.origin);
97 if event.click_count >= 2 {
100 if let PivotHitResult::Cell { row, col } = s.hit_test(rel) {
101 s.request_drill_down(row, col);
102 cx.notify();
103 return;
104 }
105 }
106 s.handle_mouse_down(rel, event.modifiers.shift);
107 cx.notify();
108 });
109 },
110 )
111 .on_mouse_down(
112 MouseButton::Right,
113 move |event: &MouseDownEvent, window, cx| {
114 window.focus(&focus_right);
115 state_right.update(cx, |s, cx| {
116 let rel = to_grid_relative(event.position, s.bounds.origin);
117 let hit = s.hit_test(rel);
118 s.open_context_menu(hit, rel);
119 cx.notify();
120 });
121 },
122 )
123 .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
124 state_move.update(cx, |s, cx| {
125 let rel = to_grid_relative(event.position, s.bounds.origin);
126 s.handle_mouse_move(rel, event.pressed_button == Some(MouseButton::Left));
127 cx.notify();
128 });
129 })
130 .on_mouse_up(
131 MouseButton::Left,
132 move |_event: &MouseUpEvent, _window, cx| {
133 state_up.update(cx, |s, cx| {
134 s.handle_mouse_up();
135 cx.notify();
136 });
137 },
138 )
139 .on_scroll_wheel(move |event: &ScrollWheelEvent, _window, cx| {
140 state_scroll.update(cx, |s, cx| {
141 let line_h = px(s.row_height);
142 let delta = event.delta.pixel_delta(line_h);
143 s.apply_scroll_delta(f32::from(delta.x), f32::from(delta.y));
144 cx.notify();
145 });
146 })
147 .on_key_down(move |event: &KeyDownEvent, _window, cx| {
148 state_key.update(cx, |s, cx| {
149 s.handle_key(&event.keystroke, cx);
150 cx.notify();
151 });
152 })
153 .children(render_pivot_menu_overlay(&self.state, cx))
154 }
155}
156
157fn render_pivot_menu_overlay(
161 state: &Entity<PivotState>,
162 cx: &mut Context<PivotGrid>,
163) -> Option<impl IntoElement + use<>> {
164 let s = state.read(cx);
165 let menu = s.menu.as_ref()?;
166 let theme = s.theme.clone();
167 let items = menu.items.clone();
168 let hovered = menu.hovered;
169 let cw = s.char_width;
170 let abs_x = f32::from(s.bounds.origin.x) + f32::from(menu.anchor.x);
171 let abs_y = f32::from(s.bounds.origin.y) + f32::from(menu.anchor.y);
172
173 let max_label_chars = items
174 .iter()
175 .filter_map(|item| match item {
176 PivotMenuItem::Action { label, .. } => Some(label.chars().count()),
177 PivotMenuItem::Separator => None,
178 })
179 .max()
180 .unwrap_or(0);
181 let menu_w = (max_label_chars as f32 * cw + MENU_PADDING_X * 2.0).max(160.0);
182
183 let mut rows: Vec<gpui::AnyElement> = Vec::with_capacity(items.len());
184 let mut action_idx = 0usize;
185 for item in &items {
186 match item {
187 PivotMenuItem::Separator => {
188 rows.push(
189 div()
190 .h(px(MENU_ITEM_HEIGHT))
191 .flex()
192 .items_center()
193 .child(div().mx(px(4.0)).h(px(1.0)).w_full().bg(theme.grid_line))
194 .into_any_element(),
195 );
196 }
197 PivotMenuItem::Action { id, label } => {
198 let this_idx = action_idx;
199 action_idx += 1;
200 let id = id.clone();
201 let state_click = state.clone();
202 let state_hover = state.clone();
203 let mut row = div()
204 .h(px(MENU_ITEM_HEIGHT))
205 .px(px(MENU_PADDING_X))
206 .flex()
207 .items_center()
208 .text_color(theme.menu_fg)
209 .text_size(px(MENU_FONT_SIZE))
210 .child(label.clone())
211 .on_mouse_move(move |_e: &MouseMoveEvent, _window, cx| {
212 state_hover.update(cx, |s, cx| {
213 if let Some(m) = s.menu.as_mut() {
214 if m.hovered != Some(this_idx) {
215 m.hovered = Some(this_idx);
216 cx.notify();
217 }
218 }
219 });
220 })
221 .on_mouse_down(
222 MouseButton::Left,
223 move |_e: &MouseDownEvent, _window, cx| {
224 state_click.update(cx, |s, cx| {
225 if let Some(menu) = s.menu.take() {
226 s.execute_menu_action(&id, menu, cx);
227 }
228 cx.notify();
229 });
230 },
231 );
232 if hovered == Some(this_idx) {
233 row = row.bg(theme.menu_hover_bg);
234 }
235 rows.push(row.into_any_element());
236 }
237 }
238 }
239
240 let menu_body = div()
241 .flex()
242 .flex_col()
243 .w(px(menu_w))
244 .py(px(MENU_INNER_PAD))
245 .bg(theme.menu_bg)
246 .border_1()
247 .border_color(theme.grid_line)
248 .children(rows);
249
250 let state_backdrop = state.clone();
251 let overlay = deferred(anchored().position(point(px(abs_x), px(abs_y))).child(
252 div().occlude().child(menu_body).on_mouse_down_out(
253 move |_e: &MouseDownEvent, _window, cx| {
254 state_backdrop.update(cx, |s, cx| {
255 if s.menu.is_some() {
256 s.menu = None;
257 cx.notify();
258 }
259 });
260 },
261 ),
262 ))
263 .with_priority(PIVOT_MENU_PRIORITY);
264 Some(overlay)
265}