use gpui::{AnyView, Corner, Entity, Pixels, Point}; use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*}; use super::PopoverMenuHandle; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub enum DropdownStyle { #[default] Solid, Outlined, Subtle, Ghost, } enum LabelKind { Text(SharedString), Element(AnyElement), } #[derive(IntoElement, RegisterComponent)] pub struct DropdownMenu { id: ElementId, label: LabelKind, trigger_size: ButtonSize, trigger_tooltip: Option AnyView + 'static>>, trigger_icon: Option, style: DropdownStyle, menu: Entity, full_width: bool, disabled: bool, handle: Option>, attach: Option, offset: Option>, tab_index: Option, chevron: bool, } impl DropdownMenu { pub fn new( id: impl Into, label: impl Into, menu: Entity, ) -> Self { Self { id: id.into(), label: LabelKind::Text(label.into()), trigger_size: ButtonSize::Default, trigger_tooltip: None, trigger_icon: Some(IconName::ChevronUpDown), style: DropdownStyle::default(), menu, full_width: false, disabled: false, handle: None, attach: None, offset: None, tab_index: None, chevron: true, } } pub fn new_with_element( id: impl Into, label: AnyElement, menu: Entity, ) -> Self { Self { id: id.into(), label: LabelKind::Element(label), trigger_size: ButtonSize::Default, trigger_tooltip: None, trigger_icon: Some(IconName::ChevronUpDown), style: DropdownStyle::default(), menu, full_width: false, disabled: false, handle: None, attach: None, offset: None, tab_index: None, chevron: true, } } pub fn style(mut self, style: DropdownStyle) -> Self { self.style = style; self } pub fn trigger_size(mut self, size: ButtonSize) -> Self { self.trigger_size = size; self } pub fn trigger_tooltip( mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, ) -> Self { self.trigger_tooltip = Some(Box::new(tooltip)); self } pub fn trigger_icon(mut self, icon: IconName) -> Self { self.trigger_icon = Some(icon); self } pub fn full_width(mut self, full_width: bool) -> Self { self.full_width = full_width; self } pub fn handle(mut self, handle: PopoverMenuHandle) -> Self { self.handle = Some(handle); self } /// Defines which corner of the handle to attach the menu's anchor to. pub fn attach(mut self, attach: Corner) -> Self { self.attach = Some(attach); self } /// Offsets the position of the menu by that many pixels. pub fn offset(mut self, offset: Point) -> Self { self.offset = Some(offset); self } pub fn tab_index(mut self, arg: isize) -> Self { self.tab_index = Some(arg); self } pub fn no_chevron(mut self) -> Self { self.chevron = false; self } } impl Disableable for DropdownMenu { fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } impl RenderOnce for DropdownMenu { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let button_style = match self.style { DropdownStyle::Solid => ButtonStyle::Filled, DropdownStyle::Subtle => ButtonStyle::Subtle, DropdownStyle::Outlined => ButtonStyle::Outlined, DropdownStyle::Ghost => ButtonStyle::Transparent, }; let full_width = self.full_width; let trigger_size = self.trigger_size; let (text_button, element_button) = match self.label { LabelKind::Text(text) => ( Some( Button::new(self.id.clone(), text) .style(button_style) .when(self.chevron, |this| { this.icon(self.trigger_icon) .icon_position(IconPosition::End) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) }) .when(full_width, |this| this.full_width()) .size(trigger_size) .disabled(self.disabled) .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), ), None, ), LabelKind::Element(element) => ( None, Some( ButtonLike::new(self.id.clone()) .child(element) .style(button_style) .when(self.chevron, |this| { this.child( Icon::new(IconName::ChevronUpDown) .size(IconSize::XSmall) .color(Color::Muted), ) }) .when(full_width, |this| this.full_width()) .size(trigger_size) .disabled(self.disabled) .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), ), ), }; let mut popover = PopoverMenu::new((self.id.clone(), "popover")) .full_width(self.full_width) .menu(move |_window, _cx| Some(self.menu.clone())); popover = match (text_button, element_button, self.trigger_tooltip) { (Some(text_button), None, Some(tooltip)) => { popover.trigger_with_tooltip(text_button, tooltip) } (Some(text_button), None, None) => popover.trigger(text_button), (None, Some(element_button), Some(tooltip)) => { popover.trigger_with_tooltip(element_button, tooltip) } (None, Some(element_button), None) => popover.trigger(element_button), _ => popover, }; popover .attach(match self.attach { Some(attach) => attach, None => Corner::BottomRight, }) .when_some(self.offset, |this, offset| this.offset(offset)) .when_some(self.handle, |this, handle| this.with_handle(handle)) } } impl Component for DropdownMenu { fn scope() -> ComponentScope { ComponentScope::Input } fn name() -> &'static str { "DropdownMenu" } fn description() -> Option<&'static str> { Some( "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).", ) } fn preview(window: &mut Window, cx: &mut App) -> Option { let menu = ContextMenu::build(window, cx, |this, _, _| { this.entry("Option 1", None, |_, _| {}) .entry("Option 2", None, |_, _| {}) .entry("Option 3", None, |_, _| {}) .separator() .entry("Option 4", None, |_, _| {}) }); let menu_with_submenu = ContextMenu::build(window, cx, |this, _, _| { this.entry("Toggle All Docks", None, |_, _| {}) .submenu("Editor Layout", |menu, _, _| { menu.entry("Split Up", None, |_, _| {}) .entry("Split Down", None, |_, _| {}) .separator() .entry("Split Side", None, |_, _| {}) }) .separator() .entry("Project Panel", None, |_, _| {}) .entry("Outline Panel", None, |_, _| {}) .separator() .submenu("Autofill", |menu, _, _| { menu.entry("Contact…", None, |_, _| {}) .entry("Passwords…", None, |_, _| {}) }) .submenu_with_icon("Predict", IconName::ZedPredict, |menu, _, _| { menu.entry("Everywhere", None, |_, _| {}) .entry("At Cursor", None, |_, _| {}) .entry("Over Here", None, |_, _| {}) .entry("Over There", None, |_, _| {}) }) }); Some( v_flex() .gap_6() .children(vec![ example_group_with_title( "Basic Usage", vec![ single_example( "Default", DropdownMenu::new("default", "Select an option", menu.clone()) .into_any_element(), ), single_example( "Full Width", DropdownMenu::new( "full-width", "Full Width Dropdown", menu.clone(), ) .full_width(true) .into_any_element(), ), ], ), example_group_with_title( "Submenus", vec![single_example( "With Submenus", DropdownMenu::new("submenu", "Submenu", menu_with_submenu) .into_any_element(), )], ), example_group_with_title( "Styles", vec![ single_example( "Outlined", DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone()) .style(DropdownStyle::Outlined) .into_any_element(), ), single_example( "Ghost", DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone()) .style(DropdownStyle::Ghost) .into_any_element(), ), ], ), example_group_with_title( "States", vec![single_example( "Disabled", DropdownMenu::new("disabled", "Disabled Dropdown", menu) .disabled(true) .into_any_element(), )], ), ]) .into_any_element(), ) } }