use std::rc::Rc;
use gpui_kit::{
AnyElement, App, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, Length,
MouseButton, ParentElement as _, RenderOnce, StyleRefinement, Styled, Window,
base::{
Align, GlobalState, StyledExt as _, TestSupportExt as _,
actions::{Confirm, SelectDown},
},
div,
prelude::FluentBuilder as _,
};
use super::{
MenuAnchor, MenuEntry, MenuState, TRIGGER_CONTEXT,
panel::{MenuPanels, measure},
};
pub(crate) type EntriesBuilder = Rc<dyn Fn(&mut Window, &mut App) -> Vec<MenuEntry>>;
type Opener = Rc<dyn Fn(bool, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct DropdownMenu {
id: ElementId,
state: Entity<MenuState>,
trigger: Option<AnyElement>,
items: Option<EntriesBuilder>,
align: Align,
action_context: Option<FocusHandle>,
menu_width: Option<Length>,
style: StyleRefinement,
}
impl DropdownMenu {
pub fn new(id: impl Into<ElementId>, state: &Entity<MenuState>) -> Self {
Self {
id: id.into(),
state: state.clone(),
trigger: None,
items: None,
align: Align::End,
action_context: None,
menu_width: None,
style: StyleRefinement::default(),
}
}
pub fn trigger(mut self, trigger: impl IntoElement) -> Self {
self.trigger = Some(trigger.into_any_element());
self
}
pub fn items(
mut self,
items: impl Fn(&mut Window, &mut App) -> Vec<MenuEntry> + 'static,
) -> Self {
self.items = Some(Rc::new(items));
self
}
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
self.action_context = Some(focus_handle.clone());
self
}
pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
self.menu_width = Some(width.into());
self
}
}
impl Styled for DropdownMenu {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for DropdownMenu {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let state = self.state;
let (open, trigger_bounds) = {
let state = state.read(cx);
(state.is_open(), state.trigger_bounds().clone())
};
let open_menu: Opener = {
let state = state.clone();
let items = self.items;
let align = self.align;
let context = self.action_context;
let trigger_bounds = trigger_bounds.clone();
Rc::new(move |keyboard, window, cx| {
let entries = items
.as_ref()
.map(|items| items(window, cx))
.unwrap_or_default();
let anchor = MenuAnchor::Trigger(trigger_bounds.get(), align);
state.update(cx, |state, cx| {
match &context {
Some(context) => state.open_for(entries, anchor, context, window, cx),
None => state.open(entries, anchor, window, cx),
}
if keyboard {
state.highlight_edge(false, cx);
}
});
})
};
div()
.id(self.id.clone())
.test_support()
.key_context(TRIGGER_CONTEXT)
.flex_shrink_0()
.refine_style(&self.style)
.relative()
.child(measure(move |bounds| trigger_bounds.set(bounds)))
.on_mouse_down(MouseButton::Left, {
let state = state.clone();
let open_menu = open_menu.clone();
move |_, window, cx| {
window.prevent_default();
cx.stop_propagation();
GlobalState::suppress_text_selection(cx);
if state.read(cx).is_open() != open {
return;
}
if open {
state.update(cx, |state, cx| state.close(window, cx));
} else {
open_menu(false, window, cx);
}
}
})
.when(!open, |this| {
let confirm = open_menu.clone();
this.on_action(move |_: &Confirm, window, cx| confirm(true, window, cx))
.on_action(move |_: &SelectDown, window, cx| open_menu(true, window, cx))
})
.children(self.trigger)
.child(MenuPanels::new(self.id, &state).width(self.menu_width))
}
}