DropdownButton
A DropdownButton is a combination of a button and a trigger button. It allows us to display a dropdown menu when the trigger is clicked, but the left Button can still respond to independent events.
Shared variant and size can be set on the DropdownButton. Action-specific options such as its label, icon, tooltip, loading state and click handler belong to the inner Button.
#Import
use gpui_kit::component::button::{Button, DropdownButton};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { DropdownButtonAction, DropdownButton } from "dropdown_button.slint";use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};#Usage
use gpui_kit::Anchor;
DropdownButton::new("dropdown")
.button(Button::new("btn").label("Click Me"))
.dropdown_menu(|menu, _, _| {
menu.menu("Option 1", Box::new(MyAction))
.menu("Option 2", Box::new(MyAction))
.separator()
.menu("Option 3", Box::new(MyAction))
})// The whole of a dropdown button: the label on the leading half, the menu on
// the trailing one, and the message that says which half reported. A menu row
// reports `DropdownAction::Menu` with its own index; the leading half reports
// `DropdownAction::Main`.
let usage: Element<Message> = Element::from(wired!(
app,
0,
DropdownButton::new("Export")
.variant(Variant::Primary)
.items([
Item::new("Export all rows (.csv)")
.icon(DOWNLOAD)
.shortcut("Cmd E"),
Item::new("Download report (.pdf)").icon(FILE_TEXT),
Item::separator(),
Item::new("Copy as Markdown").icon(COPY),
])
));
let usage_section = column![
usage,
muted_line("`on_chosen` says which half reported, and which row when it was the menu."),
]
.spacing(8.0);#Basic split
// The leading half runs its own action; the trailing half opens the menu, so
// the label is never a way to open the menu by accident. The primary variant
// is the reference's ordinary shape: the two fills join into one button with
// a trailing chevron.
let basic: Element<Message> = Element::from(wired!(
app,
1,
DropdownButton::new("Export")
.variant(Variant::Primary)
.items([
Item::new("Export all rows (.csv)").shortcut("Cmd E"),
Item::new("Download report (.pdf)"),
])
));
let basic_section = column![
basic,
muted_line("Press the label for the main action, the caret for the menu."),
]
.spacing(8.0);The control is two buttons sharing one outline. The leading half runs its own action; the trailing half opens the menu, so a click on the label never opens the menu by accident.
DropdownButton::new("dropdown")
.primary()
.button(Button::new("btn").label("Export").on_click(|_, _, _| println!("Export")))
.dropdown_menu(|menu, _, _| {
menu.menu("Export all rows (.csv)", Box::new(MyAction))
.menu("Download report (.pdf)", Box::new(MyAction))
})#Variants
// The variant and the size belong to the whole control: one value styles
// both halves, which is Slint's `variant` and GPUI's effective variant.
let variants: Element<Message> = layout_row(row![
wired!(
app,
2,
DropdownButton::new("Default").items([
Item::new("Rename").icon(PENCIL),
Item::new("Duplicate").icon(COPY),
])
),
wired!(
app,
3,
DropdownButton::new("Primary")
.variant(Variant::Primary)
.items([
Item::new("Publish now").icon(UPLOAD),
Item::new("Schedule…").icon(CLOCK),
])
),
wired!(
app,
4,
DropdownButton::new("Outline")
.variant(Variant::Primary)
.outline(true)
.selected(true)
.items([
Item::new("Export all rows (.csv)")
.icon(DOWNLOAD)
.checked(true),
Item::new("Export selection").icon(FILE_TEXT),
])
),
wired!(
app,
5,
DropdownButton::new("Small")
.variant(Variant::Secondary)
.size(ControlSize::Small)
.items([
Item::new("Compact view").icon(LIST),
Item::new("Comfortable view").icon(TABLE),
])
),
wired!(
app,
6,
DropdownButton::new("Ghost")
.variant(Variant::Ghost)
.items([
Item::new("Duplicate").icon(COPY),
Item::new("Delete").icon(TRASH),
])
),
]
.spacing(12.0)
.align_y(Alignment::Center));
let variants_section = body_box(variants);Same as Button, DropdownButton supports different variants.
DropdownButton::new("dropdown")
.primary()
.button(Button::new("btn").label("Primary"))
.dropdown_menu(|menu, _, _| {
menu.menu("Option 1", Box::new(MyAction))
})HorizontalLayout {
spacing: 12px;
alignment: start;
DropdownButton {
text: "Default";
items: [
{ label: "Rename", icon: "pencil", shortcut: "", disabled: false, checked: false, separator: false },
{ label: "Duplicate", icon: "copy", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = event == DropdownButtonAction.menu
? "Renamed from the default variant."
: "Default variant clicked.";
}
}
DropdownButton {
text: "Primary";
variant: ButtonVariant.primary;
items: [
{ label: "Publish now", icon: "upload", shortcut: "", disabled: false, checked: false, separator: false },
{ label: "Schedule…", icon: "clock", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = event == DropdownButtonAction.menu
? "Scheduled a publish."
: "Published.";
}
}
DropdownButton {
text: "Outline";
variant: ButtonVariant.primary;
outline: true;
selected: true;
items: [
{ label: "Export all rows (.csv)", icon: "download", shortcut: "", disabled: false, checked: true, separator: false },
{ label: "Export selection", icon: "file-text", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = event == DropdownButtonAction.menu
? "Outlined export chosen."
: "Outlined export clicked.";
}
}
DropdownButton {
text: "Small";
size: ControlSize.small;
variant: ButtonVariant.secondary;
items: [
{ label: "Compact view", icon: "list", shortcut: "", disabled: false, checked: false, separator: false },
{ label: "Comfortable view", icon: "table", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = "Density changed.";
}
}
DropdownButton {
text: "Ghost";
variant: ButtonVariant.ghost;
items: [
{ label: "Duplicate", icon: "copy", shortcut: "", disabled: false, checked: false, separator: false },
{ label: "Delete", icon: "trash", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = "Ghost action.";
}
}
}Leaving the variant or size unset on the DropdownButton uses the inner button’s value for both halves.
#Inner button options
DropdownButton::new("dropdown")
.button(
Button::new("btn")
.label("Save")
.compact()
.loading(is_saving)
.tooltip("Save the current view")
.on_click(|_, _, _| println!("Saved")),
)
.dropdown_menu(|menu, _, _| {
menu.menu("Save as…", Box::new(MyAction))
})HorizontalLayout {
spacing: 12px;
alignment: start;
DropdownButton {
text: "Save";
icon: "download";
tooltip: "Save the current document";
variant: ButtonVariant.primary;
items: [
{ label: "Save as new file…", icon: "file", shortcut: "⌘S", disabled: false, checked: false, separator: false },
{ label: "Save as template…", icon: "copy", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = "Document saved.";
}
}
DropdownButton {
text: "Saving…";
variant: ButtonVariant.primary;
loading: true;
items: [
{ label: "Save as new file…", icon: "file", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = "Still saving…";
}
}
DropdownButton {
text: "Unavailable";
variant: ButtonVariant.primary;
enabled: false;
items: [
{ label: "Export all rows (.csv)", icon: "download", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = "Disabled.";
}
}
}// The leading half owns its icon, its loading state and whether it answers at
// all; the whole control owns the variant and the size. The muted `Save` is
// the reference's own split: its border draws the edge between the halves,
// where the primary `Export` above joins into one button.
let inner: Element<Message> = layout_row(row![
wired!(
app,
7,
DropdownButton::new("Save")
.variant(Variant::Default)
.icon(|| icon(DOWNLOAD))
.items([
Item::new("Save as new file…").icon(FILE).shortcut("Cmd S"),
Item::new("Save as template…").icon(COPY),
])
),
wired!(
app,
8,
DropdownButton::new("Saving…")
.variant(Variant::Primary)
.loading(true)
.items([Item::new("Save as new file…").icon(FILE)])
),
wired!(
app,
9,
DropdownButton::new("Unavailable")
.variant(Variant::Primary)
.disabled(true)
.items([Item::new("Export all rows (.csv)").icon(DOWNLOAD)])
),
]
.spacing(12.0)
.align_y(Alignment::Center));
let inner_section = body_box(inner);#With custom anchor
// A control whose label is only an icon, with the caret the component draws
// itself, and a menu wider than its button. The menu hangs from the trailing
// edge, which is where the popover's bottom placement and the control's own
// width put it.
let anchors: Element<Message> = layout_row(row![
wired!(
app,
10,
DropdownButton::new("")
.icon(|| icon(PLAY))
.variant(Variant::Secondary)
.size(ControlSize::Small)
.menu_width(240.0)
.items([
Item::new("Run with release profile")
.icon(ZAP)
.shortcut("⇧Cmd R"),
Item::new("Run with debugger").icon(BOT),
Item::separator(),
Item::new("Stop").icon(SQUARE).disabled(true),
])
),
wired!(
app,
11,
DropdownButton::new("Disabled")
.variant(Variant::Secondary)
.disabled(true)
.items([Item::new("Open recent").icon(CLOCK)])
),
]
.spacing(12.0)
.align_y(Alignment::Center));
let anchors_section = body_box(anchors);The menu’s anchor and width belong to the menu, not to the button. Set them on the DropdownButton when the default placement does not fit.
DropdownButton::new("dropdown")
.button(Button::new("btn").label("Click Me"))
.dropdown_menu_with_anchor(Anchor::BottomRight, |menu, _, _| {
menu.menu("Option 1", Box::new(MyAction))
})HorizontalLayout {
spacing: 12px;
alignment: start;
DropdownButton {
text: "";
icon: "play";
tooltip: "Run the current file";
variant: ButtonVariant.secondary;
size: ControlSize.small;
menu-width: 240px;
items: [
{ label: "Run with release profile", icon: "zap", shortcut: "⇧⌘R", disabled: false, checked: false, separator: false },
{ label: "Run with debugger", icon: "bot", shortcut: "", disabled: false, checked: false, separator: false },
{ label: "", icon: "", shortcut: "", disabled: false, checked: false, separator: true },
{ label: "Stop", icon: "square", shortcut: "", disabled: true, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = event == DropdownButtonAction.menu
? "Started run \{index + 1}."
: "Running the current file.";
}
}
DropdownButton {
text: "Disabled";
variant: ButtonVariant.secondary;
enabled: false;
items: [
{ label: "Open recent", icon: "clock", shortcut: "", disabled: false, checked: false, separator: false },
];
chosen(index, event) => {
root.last-action = "Disabled control.";
}
}
}