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 animations = s.animations;
168 let items = menu.items.clone();
169 let hovered = menu.hovered;
170 let cw = s.char_width;
171 let abs_x = f32::from(s.bounds.origin.x) + f32::from(menu.anchor.x);
172 let abs_y = f32::from(s.bounds.origin.y) + f32::from(menu.anchor.y);
173
174 let max_label_chars = items
175 .iter()
176 .filter_map(|item| match item {
177 PivotMenuItem::Action { label, .. } => Some(label.chars().count()),
178 PivotMenuItem::Separator => None,
179 })
180 .max()
181 .unwrap_or(0);
182 let menu_w = (max_label_chars as f32 * cw + MENU_PADDING_X * 2.0).max(160.0);
183
184 let mut rows: Vec<gpui::AnyElement> = Vec::with_capacity(items.len());
185 let mut action_idx = 0usize;
186 for item in &items {
187 match item {
188 PivotMenuItem::Separator => {
189 rows.push(
190 div()
191 .h(px(MENU_ITEM_HEIGHT))
192 .flex()
193 .items_center()
194 .child(div().mx(px(4.0)).h(px(1.0)).w_full().bg(theme.grid_line))
195 .into_any_element(),
196 );
197 }
198 PivotMenuItem::Action { id, label } => {
199 let this_idx = action_idx;
200 action_idx += 1;
201 let id = id.clone();
202 let state_click = state.clone();
203 let state_hover = state.clone();
204 let mut row = div()
205 .h(px(MENU_ITEM_HEIGHT))
206 .px(px(MENU_PADDING_X))
207 .flex()
208 .items_center()
209 .text_color(theme.menu_fg)
210 .text_size(px(MENU_FONT_SIZE))
211 .child(label.clone())
212 .on_mouse_move(move |_e: &MouseMoveEvent, _window, cx| {
213 state_hover.update(cx, |s, cx| {
214 if let Some(m) = s.menu.as_mut() {
215 if m.hovered != Some(this_idx) {
216 m.hovered = Some(this_idx);
217 cx.notify();
218 }
219 }
220 });
221 })
222 .on_mouse_down(
223 MouseButton::Left,
224 move |_e: &MouseDownEvent, _window, cx| {
225 state_click.update(cx, |s, cx| {
226 if let Some(menu) = s.menu.take() {
227 s.execute_menu_action(&id, menu, cx);
228 }
229 cx.notify();
230 });
231 },
232 );
233 if hovered == Some(this_idx) {
234 row = row.bg(theme.menu_hover_bg);
235 }
236 rows.push(row.into_any_element());
237 }
238 }
239 }
240
241 let menu_body = div()
242 .flex()
243 .flex_col()
244 .w(px(menu_w))
245 .py(px(MENU_INNER_PAD))
246 .bg(theme.menu_bg)
247 .border_1()
248 .border_color(theme.grid_line)
249 .children(rows);
250
251 let state_backdrop = state.clone();
252 let overlay = deferred(
253 anchored().position(point(px(abs_x), px(abs_y))).child(
254 div()
255 .occlude()
256 .child(crate::grid::motion::pop_in(
257 menu_body,
258 "pivot-context-menu",
259 animations,
260 ))
261 .on_mouse_down_out(move |_e: &MouseDownEvent, _window, cx| {
262 state_backdrop.update(cx, |s, cx| {
263 if s.menu.is_some() {
264 s.menu = None;
265 cx.notify();
266 }
267 });
268 }),
269 ),
270 )
271 .with_priority(PIVOT_MENU_PRIORITY);
272 Some(overlay)
273}