1use std::{
5 cell::{Cell, RefCell},
6 collections::HashMap,
7 rc::Rc,
8};
9
10use gpui_kit::{
11 App, Bounds, Context, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, ListState,
12 Pixels, Point, SharedString, Subscription, Window,
13 base::{Align, DeferredPopover, GlobalState, Placement},
14};
15
16use super::entry::{MenuEntry, PendingActivation};
17use crate::ActiveTheme as _;
18
19#[derive(Clone, Copy, Debug, PartialEq)]
21pub enum MenuAnchor {
22 Trigger(Bounds<Pixels>, Align),
25 Point(Point<Pixels>),
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum MenuEvent {
34 Opened,
36 Closed,
38 Activated(SharedString),
40}
41
42pub(crate) struct Level {
45 parent_row: Option<usize>,
48 anchor: Bounds<Pixels>,
51 highlighted: Option<usize>,
52 list: ListState,
53 bounds: Rc<Cell<Bounds<Pixels>>>,
56 submenu_rows: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
59}
60
61impl Level {
62 fn new(
63 parent_row: Option<usize>,
64 anchor: Bounds<Pixels>,
65 rows: usize,
66 overdraw: Pixels,
67 ) -> Self {
68 Self {
69 parent_row,
70 anchor,
71 highlighted: None,
72 list: ListState::new(rows, ListAlignment::Top, overdraw),
75 bounds: Rc::new(Cell::new(Bounds::default())),
76 submenu_rows: Rc::new(RefCell::new(HashMap::new())),
77 }
78 }
79
80 pub(crate) fn list(&self) -> &ListState {
81 &self.list
82 }
83
84 pub(crate) fn anchor(&self) -> Bounds<Pixels> {
85 self.anchor
86 }
87
88 pub(crate) fn bounds(&self) -> &Rc<Cell<Bounds<Pixels>>> {
89 &self.bounds
90 }
91
92 pub(crate) fn submenu_rows(&self) -> &Rc<RefCell<HashMap<usize, Bounds<Pixels>>>> {
93 &self.submenu_rows
94 }
95
96 pub(crate) fn highlighted(&self) -> Option<usize> {
97 self.highlighted
98 }
99}
100
101struct OpenMenu {
104 entries: Vec<MenuEntry>,
105 anchor: MenuAnchor,
106 placement: Placement,
109 levels: Vec<Level>,
111 restore_focus: Option<FocusHandle>,
112 action_context: Option<FocusHandle>,
113 _deferred: DeferredPopover,
116 _focus_watch: Subscription,
117}
118
119pub struct MenuState {
130 open: Option<OpenMenu>,
131 focus: FocusHandle,
132 trigger_bounds: Rc<Cell<Bounds<Pixels>>>,
134 step_handler: Option<StepHandler>,
135}
136
137pub(crate) type StepHandler = Rc<dyn Fn(isize, &mut Window, &mut App)>;
139
140impl EventEmitter<MenuEvent> for MenuState {}
141
142impl Focusable for MenuState {
144 fn focus_handle(&self, _: &App) -> FocusHandle {
145 self.focus.clone()
146 }
147}
148
149impl MenuState {
150 pub fn new(cx: &mut Context<Self>) -> Self {
152 Self {
153 open: None,
154 focus: cx.focus_handle(),
155 trigger_bounds: Rc::new(Cell::new(Bounds::default())),
156 step_handler: None,
157 }
158 }
159
160 pub fn is_open(&self) -> bool {
162 self.open.is_some()
163 }
164
165 pub fn highlighted(&self) -> Option<SharedString> {
168 let open = self.open.as_ref()?;
169 let depth = open.active_depth();
170 let row = open.levels[depth].highlighted?;
171 open.entries_at(depth)?.get(row)?.key().cloned()
172 }
173
174 pub fn open(
180 &mut self,
181 entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
182 anchor: MenuAnchor,
183 window: &mut Window,
184 cx: &mut Context<Self>,
185 ) {
186 let context = self.restore_target(window, cx);
187 if context.is_none() {
188 let this = cx.weak_entity();
191 window.on_next_frame(move |_, cx| {
192 this.update(cx, |_, cx| cx.notify()).ok();
193 });
194 }
195 self.open_with_context(entries, anchor, context, window, cx);
196 }
197
198 pub fn set_entries(
202 &mut self,
203 entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
204 cx: &mut Context<Self>,
205 ) {
206 let Some(open) = &mut self.open else {
207 return;
208 };
209 let highlighted = open.levels[0]
210 .highlighted
211 .and_then(|row| open.entries.get(row))
212 .and_then(|entry| entry.key().cloned());
213 open.entries = normalize(entries.into_iter().map(Into::into).collect());
214 open.levels.truncate(1);
215 let root = &mut open.levels[0];
216 root.list.reset(open.entries.len());
217 root.highlighted = highlighted.and_then(|key| {
218 open.entries
219 .iter()
220 .position(|entry| entry.key() == Some(&key) && entry.is_selectable())
221 });
222 if let Some(row) = root.highlighted {
223 root.list.scroll_to_reveal_item(row);
224 }
225 cx.notify();
226 }
227
228 pub fn open_for(
232 &mut self,
233 entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
234 anchor: MenuAnchor,
235 action_context: &FocusHandle,
236 window: &mut Window,
237 cx: &mut Context<Self>,
238 ) {
239 self.open_with_context(entries, anchor, Some(action_context.clone()), window, cx);
240 }
241
242 pub fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
244 let Some(open) = self.open.take() else {
245 return;
246 };
247 if let Some(focus) = open.restore_focus {
248 focus.focus(window, cx);
249 }
250 cx.emit(MenuEvent::Closed);
251 cx.notify();
252 }
253
254 fn restore_target(&self, window: &Window, cx: &App) -> Option<FocusHandle> {
257 match &self.open {
258 Some(open) => open.restore_focus.clone(),
259 None => window.focused(cx),
260 }
261 }
262
263 fn open_with_context(
264 &mut self,
265 entries: impl IntoIterator<Item = impl Into<MenuEntry>>,
266 anchor: MenuAnchor,
267 action_context: Option<FocusHandle>,
268 window: &mut Window,
269 cx: &mut Context<Self>,
270 ) {
271 let restore_focus = self.restore_target(window, cx);
272 let entries = normalize(entries.into_iter().map(Into::into).collect());
273 let (max_height, gap) = {
274 let metrics = &cx.theme().metrics;
275 (metrics.menu_max_height, metrics.menu_gap)
276 };
277 let placement = match anchor {
278 MenuAnchor::Trigger(bounds, _) => {
279 super::placement(bounds, window.viewport_size().height, max_height, gap)
280 }
281 MenuAnchor::Point(_) => Placement::Bottom,
282 };
283 let root = Level::new(None, Bounds::default(), entries.len(), max_height);
284 let focus_watch = cx.on_focus_out(&self.focus, window, |this, _, window, cx| {
285 this.close(window, cx);
286 });
287 let was_open = self.open.is_some();
288 self.open = Some(OpenMenu {
289 entries,
290 anchor,
291 placement,
292 levels: vec![root],
293 restore_focus,
294 action_context,
295 _deferred: GlobalState::register_deferred_popover(cx),
296 _focus_watch: focus_watch,
297 });
298 self.focus.focus(window, cx);
299 if !was_open {
300 cx.emit(MenuEvent::Opened);
301 }
302 cx.notify();
303 }
304
305 pub(crate) fn trigger_bounds(&self) -> &Rc<Cell<Bounds<Pixels>>> {
306 &self.trigger_bounds
307 }
308
309 pub(crate) fn anchor(&self) -> Option<(MenuAnchor, Placement)> {
310 let open = self.open.as_ref()?;
311 Some((open.anchor, open.placement))
312 }
313
314 pub(crate) fn levels(&self) -> &[Level] {
315 self.open.as_ref().map_or(&[], |open| &open.levels)
316 }
317
318 pub(crate) fn entries_at(&self, depth: usize) -> Option<&[MenuEntry]> {
320 self.open.as_ref()?.entries_at(depth)
321 }
322
323 pub(crate) fn open_child_row(&self, depth: usize) -> Option<usize> {
325 self.open.as_ref()?.levels.get(depth + 1)?.parent_row
326 }
327
328 pub(crate) fn action_context(&self) -> &FocusHandle {
331 self.open
332 .as_ref()
333 .and_then(|open| open.action_context.as_ref())
334 .unwrap_or(&self.focus)
335 }
336
337 pub(crate) fn contains(&self, position: Point<Pixels>) -> bool {
339 self.levels()
340 .iter()
341 .any(|level| level.bounds.get().contains(&position))
342 }
343
344 pub(crate) fn hover_row(&mut self, depth: usize, row: usize, cx: &mut Context<Self>) {
347 let Some(open) = &mut self.open else {
348 return;
349 };
350 if !open.is_selectable(depth, row) {
351 return;
352 }
353 let unchanged = open.levels.get(depth).and_then(|level| level.highlighted) == Some(row)
354 && (open.levels.len() == depth + 1 || open.levels[depth + 1].parent_row == Some(row));
355 if unchanged {
356 return;
357 }
358 open.levels.truncate(depth + 1);
359 open.levels[depth].highlighted = Some(row);
360 open.open_submenu(depth, row, cx);
361 cx.notify();
362 }
363
364 pub(crate) fn move_highlight(&mut self, delta: isize, cx: &mut Context<Self>) {
367 let Some(open) = &mut self.open else {
368 return;
369 };
370 let depth = open.active_depth();
371 let count = open.entries_at(depth).map_or(0, <[_]>::len);
372 if count == 0 {
373 return;
374 }
375 let start = open.levels[depth]
376 .highlighted
377 .map(|row| row as isize)
378 .unwrap_or(if delta > 0 { -1 } else { count as isize });
379 let mut row = start;
380 for _ in 0..count {
381 row = (row + delta).rem_euclid(count as isize);
382 if open.is_selectable(depth, row as usize) {
383 open.highlight(depth, row as usize);
384 cx.notify();
385 return;
386 }
387 }
388 }
389
390 pub(crate) fn highlight_edge(&mut self, last: bool, cx: &mut Context<Self>) {
392 let Some(open) = &mut self.open else {
393 return;
394 };
395 let depth = open.active_depth();
396 let count = open.entries_at(depth).map_or(0, <[_]>::len);
397 let mut rows: Box<dyn Iterator<Item = usize>> = if last {
398 Box::new((0..count).rev())
399 } else {
400 Box::new(0..count)
401 };
402 if let Some(row) = rows.find(|row| open.is_selectable(depth, *row)) {
403 open.highlight(depth, row);
404 cx.notify();
405 }
406 }
407
408 pub(crate) fn enter_submenu(&mut self, cx: &mut Context<Self>) -> bool {
411 let Some(open) = &mut self.open else {
412 return false;
413 };
414 let depth = open.active_depth();
415 let Some(row) = open.levels[depth].highlighted else {
416 return false;
417 };
418 let is_submenu = open
419 .entries_at(depth)
420 .and_then(|entries| entries.get(row))
421 .is_some_and(|entry| entry.submenu().is_some() && entry.is_selectable());
422 if !is_submenu {
423 return false;
424 }
425 open.levels.truncate(depth + 1);
426 open.open_submenu(depth, row, cx);
427 let child = depth + 1;
428 if let Some(first) = (0..open.entries_at(child).map_or(0, <[_]>::len))
429 .find(|r| open.is_selectable(child, *r))
430 {
431 open.highlight(child, first);
432 }
433 cx.notify();
434 true
435 }
436
437 pub(crate) fn leave_submenu(&mut self, cx: &mut Context<Self>) -> bool {
440 let Some(open) = &mut self.open else {
441 return false;
442 };
443 let depth = open.active_depth();
444 if depth == 0 {
445 return false;
446 }
447 open.levels.truncate(depth);
448 cx.notify();
449 true
450 }
451
452 pub(crate) fn set_step_handler(&mut self, handler: StepHandler) {
455 self.step_handler = Some(handler);
456 }
457
458 pub(crate) fn step(this: &Entity<Self>, delta: isize, window: &mut Window, cx: &mut App) {
459 let handler = this.read(cx).step_handler.clone();
460 if let Some(handler) = handler {
461 handler(delta, window, cx);
462 }
463 }
464
465 pub(crate) fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
468 match &mut self.open {
469 Some(open) if open.levels.len() > 1 => {
470 open.levels.pop();
471 cx.notify();
472 }
473 _ => self.close(window, cx),
474 }
475 }
476
477 pub(crate) fn choose(
481 &mut self,
482 depth: usize,
483 row: usize,
484 window: &mut Window,
485 cx: &mut Context<Self>,
486 ) -> Option<PendingActivation> {
487 let open = self.open.as_mut()?;
488 if !open.is_selectable(depth, row) {
489 return None;
490 }
491 let entry = open.entries_at(depth)?.get(row)?.clone();
492 match entry {
493 MenuEntry::Submenu(_) => {
494 if open
495 .levels
496 .get(depth + 1)
497 .and_then(|level| level.parent_row)
498 != Some(row)
499 {
500 open.levels.truncate(depth + 1);
501 open.levels[depth].highlighted = Some(row);
502 open.open_submenu(depth, row, cx);
503 cx.notify();
504 }
505 None
506 }
507 MenuEntry::Item(item) => {
508 let pending = item.pending(
509 open.action_context
510 .clone()
511 .unwrap_or_else(|| self.focus.clone()),
512 );
513 cx.emit(MenuEvent::Activated(item.key().clone()));
514 self.close(window, cx);
515 Some(pending)
516 }
517 MenuEntry::Separator | MenuEntry::Label(_) => None,
518 }
519 }
520
521 pub(crate) fn confirm(
524 &mut self,
525 window: &mut Window,
526 cx: &mut Context<Self>,
527 ) -> Option<PendingActivation> {
528 if self.enter_submenu(cx) {
529 return None;
530 }
531 let open = self.open.as_ref()?;
532 let depth = open.active_depth();
533 let row = open.levels[depth].highlighted?;
534 self.choose(depth, row, window, cx)
535 }
536
537 pub(crate) fn activate(
541 this: &Entity<Self>,
542 choose: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> Option<PendingActivation>,
543 window: &mut Window,
544 cx: &mut App,
545 ) {
546 let pending = this.update(cx, |state, cx| choose(state, window, cx));
547 if let Some(pending) = pending {
548 pending.run(window, cx);
549 }
550 }
551}
552
553impl OpenMenu {
554 fn entries_at(&self, depth: usize) -> Option<&[MenuEntry]> {
556 let mut entries = self.entries.as_slice();
557 for level in self.levels.get(1..=depth)? {
558 entries = entries.get(level.parent_row?)?.submenu()?.children();
559 }
560 Some(entries)
561 }
562
563 fn active_depth(&self) -> usize {
566 self.levels
567 .iter()
568 .rposition(|level| level.highlighted.is_some())
569 .unwrap_or(0)
570 }
571
572 fn is_selectable(&self, depth: usize, row: usize) -> bool {
573 self.entries_at(depth)
574 .and_then(|entries| entries.get(row))
575 .is_some_and(MenuEntry::is_selectable)
576 }
577
578 fn highlight(&mut self, depth: usize, row: usize) {
581 self.levels.truncate(depth + 1);
582 let level = &mut self.levels[depth];
583 level.highlighted = Some(row);
584 level.list.scroll_to_reveal_item(row);
585 }
586
587 fn open_submenu(&mut self, depth: usize, row: usize, cx: &App) {
590 let Some(count) = self
591 .entries_at(depth)
592 .and_then(|entries| entries.get(row))
593 .and_then(MenuEntry::submenu)
594 .map(|submenu| submenu.children().len())
595 else {
596 return;
597 };
598 let level = &self.levels[depth];
599 let row_bounds = level
600 .submenu_rows
601 .borrow()
602 .get(&row)
603 .copied()
604 .unwrap_or_default();
605 let panel = level.bounds.get();
606 let anchor = Bounds::from_corners(
607 gpui_kit::point(panel.left(), row_bounds.top()),
608 gpui_kit::point(panel.right(), row_bounds.bottom()),
609 );
610 let overdraw = cx.theme().metrics.menu_max_height;
611 self.levels
612 .push(Level::new(Some(row), anchor, count, overdraw));
613 }
614}
615
616pub(crate) fn normalize(entries: Vec<MenuEntry>) -> Vec<MenuEntry> {
620 let mut groups: Vec<Vec<MenuEntry>> = vec![Vec::new()];
621 for entry in entries {
622 match entry {
623 MenuEntry::Separator => groups.push(Vec::new()),
624 MenuEntry::Submenu(submenu) => groups
625 .last_mut()
626 .expect("one group at least")
627 .push(MenuEntry::Submenu(submenu.normalized())),
628 entry => groups.last_mut().expect("one group at least").push(entry),
629 }
630 }
631 let mut rows = Vec::new();
632 for group in groups.into_iter().filter(|group| {
633 group
634 .iter()
635 .any(|entry| matches!(entry, MenuEntry::Item(_) | MenuEntry::Submenu(_)))
636 }) {
637 if !rows.is_empty() {
638 rows.push(MenuEntry::Separator);
639 }
640 rows.extend(group);
641 }
642 rows
643}