feat: add dropdown menu component

This commit is contained in:
2026-11-08 17:03:38 +07:00
parent 11b29dc91a
commit d8663593b6
10 changed files with 1244 additions and 197 deletions
+184
View File
@@ -18,6 +18,7 @@ use gpui_native_core::command_palette::{Command, CommandPalette, CommandPaletteS
use gpui_native_core::common::{Sizable as _, Size, h_flex, v_flex};
use gpui_native_core::context_menu::ContextMenu;
use gpui_native_core::dialog::Dialog;
use gpui_native_core::dropdown_menu::DropdownMenu;
use gpui_native_core::empty_state::EmptyState;
use gpui_native_core::field::{Field, FieldVariant};
use gpui_native_core::input::{Input, InputState};
@@ -106,6 +107,7 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
("dialog", "Dialog"),
("command-palette", "Command palette"),
("menu", "Menu"),
("dropdown-menu", "Dropdown menu"),
("context-menu", "Context menu"),
("popover", "Popover"),
("tooltip", "Tooltip"),
@@ -612,6 +614,16 @@ impl Gallery {
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("dropdown-menu", "Dropdown menu", |_, cx| {
cx.new(|_| DropdownMenuDemo {
open_menu: None,
status: None,
})
.into()
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("context-menu", "Context menu", |_, cx| {
cx.new(|_| ContextMenuDemo {
@@ -2690,6 +2702,178 @@ impl Render for MenuDemo {
}
}
#[derive(Clone, Copy, PartialEq)]
enum DemoDropdown {
Export,
Save,
Duplicate,
Delete,
More,
}
struct DropdownMenuDemo {
open_menu: Option<DemoDropdown>,
status: Option<SharedString>,
}
impl DropdownMenuDemo {
fn set_open(&mut self, menu: DemoDropdown, open: bool, cx: &mut Context<Self>) {
self.open_menu = open.then_some(menu);
cx.notify();
}
fn set_status(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
self.status = Some(message.into());
cx.notify();
}
}
impl Render for DropdownMenuDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let colors = cx.theme().colors;
let status: SharedString = match &self.status {
Some(message) => message.clone(),
None => "click the Save button's body; open its menu from the chevron".into(),
};
let export = DropdownMenu::new("dropdown-demo-export")
.label("Export")
.open(self.open_menu == Some(DemoDropdown::Export))
.on_open_change({
let view = view.clone();
move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.set_open(DemoDropdown::Export, open, cx);
})
}
})
.items(|_, _| {
vec![
MenuItem::new("PNG").handler(|_, cx| {
eprintln!("dropdown: export png");
let _ = cx;
}),
MenuItem::new("SVG"),
MenuItem::new("PDF"),
]
});
let save = DropdownMenu::new("dropdown-demo-save")
.label("Save")
.open(self.open_menu == Some(DemoDropdown::Save))
.on_click({
let view = view.clone();
move |_, _, cx| {
view.update(cx, |demo, cx| {
demo.set_status("saved document", cx);
});
}
})
.on_open_change({
let view = view.clone();
move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.set_open(DemoDropdown::Save, open, cx);
})
}
})
.items(|_, _| {
vec![
MenuItem::new("Save as template"),
MenuItem::new("Save a copy"),
MenuItem::separator(),
MenuItem::new("Export"),
]
});
let duplicate = DropdownMenu::new("dropdown-demo-duplicate")
.label("Duplicate")
.variant(ButtonVariant::Secondary)
.open(self.open_menu == Some(DemoDropdown::Duplicate))
.on_open_change({
let view = view.clone();
move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.set_open(DemoDropdown::Duplicate, open, cx);
})
}
})
.items(|_, _| {
vec![
MenuItem::new("Duplicate window"),
MenuItem::new("Duplicate tab"),
]
});
let delete = DropdownMenu::new("dropdown-demo-delete")
.label("Delete")
.variant(ButtonVariant::Destructive)
.open(self.open_menu == Some(DemoDropdown::Delete))
.on_open_change({
let view = view.clone();
move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.set_open(DemoDropdown::Delete, open, cx);
})
}
})
.items(|_, _| {
vec![
MenuItem::new("Delete file").destructive(true),
MenuItem::separator(),
MenuItem::new("Move to trash"),
]
});
let more = DropdownMenu::new("dropdown-demo-more")
.label("More")
.variant(ButtonVariant::Ghost)
.open(self.open_menu == Some(DemoDropdown::More))
.on_open_change({
let view = view.clone();
move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.set_open(DemoDropdown::More, open, cx);
})
}
})
.items(|_, _| {
vec![
MenuItem::new("Copy link"),
MenuItem::new("Share"),
MenuItem::new("Properties"),
]
});
ScrollArea::new("dropdown-menu-demo")
.size_full()
.p_4()
.child(
v_flex()
.gap_3()
.child(demo_section(
"menu-only trigger and split button",
colors,
h_flex().gap_3().child(export).child(save),
))
.child(demo_section(
"trigger variants",
colors,
h_flex().gap_3().child(duplicate).child(delete).child(more),
))
.child(demo_section(
"status",
colors,
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(status),
)),
)
}
}
struct ContextMenuDemo {
action: SharedString,
}