1use std::rc::Rc;
2
3use gpui::{
4 Anchor, App, Context, DismissEvent, ElementId, Entity, Focusable, InteractiveElement,
5 IntoElement, RenderOnce, SharedString, StyleRefinement, Styled, Window, prelude::FluentBuilder,
6};
7
8use crate::{Selectable, button::Button, menu::PopupMenu, popover::Popover};
9
10pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
12 fn dropdown_menu(
14 self,
15 f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
16 ) -> DropdownMenuPopover<Self> {
17 self.dropdown_menu_with_anchor(Anchor::TopLeft, f)
18 }
19
20 fn dropdown_menu_with_anchor(
22 mut self,
23 anchor: impl Into<Anchor>,
24 f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
25 ) -> DropdownMenuPopover<Self> {
26 let style = self.style().clone();
27 let id = self.interactivity().element_id.clone();
28
29 DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
30 }
31}
32
33impl DropdownMenu for Button {}
34
35#[derive(IntoElement)]
36pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
37 id: ElementId,
38 style: StyleRefinement,
39 anchor: Anchor,
40 trigger: T,
41 builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
42 on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
43}
44
45impl<T> DropdownMenuPopover<T>
46where
47 T: Selectable + IntoElement + 'static,
48{
49 fn new(
50 id: ElementId,
51 anchor: impl Into<Anchor>,
52 trigger: T,
53 builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
54 ) -> Self {
55 Self {
56 id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
57 style: StyleRefinement::default(),
58 anchor: anchor.into(),
59 trigger,
60 builder: Rc::new(builder),
61 on_open_change: None,
62 }
63 }
64
65 pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
67 self.anchor = anchor.into();
68 self
69 }
70
71 fn trigger_style(mut self, style: StyleRefinement) -> Self {
73 self.style = style;
74 self
75 }
76
77 pub fn on_open_change(
81 mut self,
82 callback: impl Fn(&bool, &mut Window, &mut App) + 'static,
83 ) -> Self {
84 self.on_open_change = Some(Rc::new(callback));
85 self
86 }
87}
88
89#[derive(Default)]
90struct DropdownMenuState {
91 menu: Option<Entity<PopupMenu>>,
92}
93
94impl<T> RenderOnce for DropdownMenuPopover<T>
95where
96 T: Selectable + IntoElement + 'static,
97{
98 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
99 let builder = self.builder.clone();
100 let menu_state =
101 window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default());
102
103 Popover::new(SharedString::from(format!("popover:{}", self.id)))
104 .appearance(false)
105 .overlay_closable(false)
106 .trigger(self.trigger)
107 .trigger_style(self.style)
108 .anchor(self.anchor)
109 .when_some(self.on_open_change, |this, callback| {
110 this.on_open_change(move |open, window, cx| callback(open, window, cx))
111 })
112 .content(move |_, window, cx| {
113 let menu = match menu_state.read(cx).menu.clone() {
120 Some(menu) => menu,
121 None => {
122 let builder = builder.clone();
123 let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
124 builder(menu, window, cx)
125 });
126 menu_state.update(cx, |state, _| {
127 state.menu = Some(menu.clone());
128 });
129 menu.focus_handle(cx).focus(window, cx);
130
131 let popover_state = cx.entity();
133 window
134 .subscribe(&menu, cx, {
135 let menu_state = menu_state.clone();
136 move |_, _: &DismissEvent, window, cx| {
137 popover_state.update(cx, |state, cx| {
138 state.dismiss(window, cx);
139 });
140 menu_state.update(cx, |state, _| {
141 state.menu = None;
142 });
143 }
144 })
145 .detach();
146
147 menu.clone()
148 }
149 };
150
151 menu.clone()
152 })
153 }
154}