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.

Install

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))
    })
SlintNo Slint version of this example yet.
// 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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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))
    })
SlintNo Slint version of this example yet.

#Variants

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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.";
        }
    }
}