Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Collections

Dropdown

Display a menu of actions anchored to a trigger.

Rust
use herogpui::components::dropdown::{Dropdown, MenuItem};

Dropdown on docs.rsSource at v0.14.0

Usage

Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.

Usage

The menu positions against the measured trigger: all 22 union placements flip to the side with more room when the preferred side cannot fit, keep a 12px cross-axis viewport inset, and cap the scroller to the available height with short menus keeping their natural height. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

let is_open = self.dropdown_open;
let selected = self
    .dropdown_selected
    .clone()
    .unwrap_or_else(|| SharedString::from("none"));
let items = vec![
    MenuItem::new("new", "New file").shortcut("Ctrl N"),
    MenuItem::new("copy", "Copy link").shortcut("Ctrl C"),
    MenuItem::Separator,
    MenuItem::new("delete", "Delete file").danger(),
];

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::new(
            "dropdown-trigger-dd",
            Button::new("dropdown-trigger")
                .label("Actions")
                .variant(Variant::Secondary),
            items,
            is_open,
        )
        .id("dropdown-trigger-dd")
        .radius(px(4.))
        .on_open_change(cx.listener(|this, open: &bool, _, cx| {
            this.dropdown_open = *open;
            cx.notify();
        }))
        .on_action(cx.listener(|this, key: &SharedString, _, cx| {
            this.dropdown_selected = Some(key.clone());
            this.dropdown_open = false;
            cx.notify();
        }))
        .into_any_element(),
        para(&format!("Last action: {selected}"), cx),
    ])

Also calls para (included when you copy).

Row Hover

`row_hover_bg` names the fill a hovered menu row takes, in place of `--default`. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let plain = || {
    vec![
        MenuItem::new("new", "New file"),
        MenuItem::new("open", "Open file"),
        MenuItem::new("save", "Save"),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Dropdown::uncontrolled(
        "dd-hover-trigger",
        Button::new("dropdown-hover")
            .label("Actions")
            .variant(Variant::Secondary),
        plain(),
    )
    .row_hover_bg(cx.colors().accent.soft())
    .into_any_element()])

Standalone Compact Menu

An embedded panel with 28px rows, 12px text and caller-owned dismissal. Its submenu inherits presentation and actions. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, Menu, MenuItem};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("dropdown-standalone-show").label("Show menu")
            .on_press(cx.listener(|this, _, _, cx| { this.set_demo_flag("dd-standalone", true); cx.notify(); })).into_any_element(),
        if self.demo_flag("dd-standalone", false) {
            Menu::new("dropdown-standalone", vec![MenuItem::new("tools", "Tools")
                .submenu(vec![MenuItem::new("copy", "Copy")])])
                .panel_min_width(px(180.)).panel_max_width(px(240.)).panel_max_height(px(160.))
                .row_height(px(28.)).row_padding_x(px(8.)).row_padding_y(px(2.))
                .row_text_size(px(12.)).row_gap(px(8.)).panel_padding(px(4.)).panel_gap(px(0.))
                .animate_entry(false)
                .row_hover_bg(cx.colors().accent.color)
                .row_hover_foreground(cx.colors().accent.foreground)
                .on_action(cx.listener(|this, key: &SharedString, _, cx| { this.dropdown_selected = Some(key.clone()); cx.notify(); }))
                .on_dismiss(cx.listener(|this, _: &bool, _, cx| { this.set_demo_flag("dd-standalone", false); cx.notify(); }))
                .into_any_element()
        } else { gpui::div().into_any_element() },
    ])

Inset Separator

`separator_inset` and `separator_thickness` (on `Menu`, or on `MenuStyle` for a whole theme) give the rule a per-edge inset inside wider panel bounds and an explicit thickness. Unset, the rule keeps v3's proportional inset: 94% of the panel width, centred. The values below are AppKit's own menu separator: 15pt in on each side, 1pt thick. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Dropdown, Menu, MenuItem};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("dropdown-inset-show").label("Show menu")
            .on_press(cx.listener(|this, _, _, cx| { this.set_demo_flag("dd-inset", true); cx.notify(); })).into_any_element(),
        if self.demo_flag("dd-inset", false) {
            Menu::new("dropdown-inset", vec![
                MenuItem::new("cut", "Cut"),
                MenuItem::new("copy", "Copy"),
                MenuItem::Separator,
                MenuItem::new("paste", "Paste"),
            ])
                .panel_min_width(px(200.)).panel_max_width(px(200.))
                .separator_inset(px(15.))
                .separator_thickness(px(1.))
                .animate_entry(false)
                .on_dismiss(cx.listener(|this, _: &bool, _, cx| { this.set_demo_flag("dd-inset", false); cx.notify(); }))
                .into_any_element()
        } else { gpui::div().into_any_element() },
    ])

Row With An Inline Control

`MenuItem::is_interactive(true)` hands the row's interaction to the element `item_content` draws for it, so a trailing secondary control answers the press instead of the row picking and closing the menu. The row stays a keyboard stop; Enter and Space belong to the control. A popup the control opens needs no extra wiring — it becomes the topmost overlay, and the menu's own dismissals stand down while it is up. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Dropdown, Menu, MenuItem, PickerItem, Select};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Button::new("dropdown-inline-show").label("Show menu")
            .on_press(cx.listener(|this, _, _, cx| { this.set_demo_flag("dd-inline", true); cx.notify(); })).into_any_element(),
        if self.demo_flag("dd-inline", false) {
            Menu::new("dropdown-inline", vec![
                MenuItem::new("reload", "Reload"),
                MenuItem::new("layout", "Layout").is_interactive(true),
            ])
                .panel_min_width(px(260.)).panel_max_width(px(260.))
                .animate_entry(false)
                .item_content(|key, _| match key.as_ref() {
                    "layout" => gpui::div()
                        .flex()
                        .items_center()
                        .justify_between()
                        .gap(px(8.))
                        .w_full()
                        .child("Layout")
                        .child(
                            Select::new(
                                "dropdown-inline-layout",
                                vec![
                                    PickerItem::new("side", "Side by side"),
                                    PickerItem::new("stack", "Stacked"),
                                ],
                            )
                            .default_value(Some("side".into()))
                            .height(px(24.))
                            .padding_x(px(6.))
                            .trigger_text_size(px(12.)),
                        )
                        .into_any_element(),
                    other => gpui::div().child(other.to_owned()).into_any_element(),
                })
                .on_dismiss(cx.listener(|this, _: &bool, _, cx| { this.set_demo_flag("dd-inline", false); cx.notify(); }))
                .into_any_element()
        } else { gpui::div().into_any_element() },
    ])

With Icons

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Variant, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Dropdown::uncontrolled(
        "dd-icons-dd",
        Button::new("dropdown-icons")
            .label("File")
            .variant(Variant::Secondary),
        vec![
            MenuItem::new("new", "New file").icon(icons::PLUS),
            MenuItem::new("copy", "Copy").icon(icons::COPY),
            MenuItem::new("delete", "Delete")
                .icon(icons::CLOSE)
                .danger(),
        ],
    )
    .id("dropdown-icons-dd")
    .into_any_element()])

With Descriptions

Labels use 14px text with 20px lines; descriptions and section headers use 12px text with 16px lines. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Placement, Variant};

Dropdown::uncontrolled(
    "dd-desc-dd",
    Button::new("dropdown-desc")
        .label("Merge")
        .variant(Variant::Secondary),
    vec![
        MenuItem::new("merge", "Create a merge commit").description(
            "All commits from this branch are added to the base branch",
        ),
        MenuItem::new("squash", "Squash and merge")
            .description("The commits are combined into one"),
        MenuItem::new("rebase", "Rebase and merge")
            .description("The commits are rebased onto the base branch"),
    ],
)
.id("dropdown-desc-dd")
.placement(Placement::BottomStart)

With Disabled Items

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::SharedString;

let plain = || {
    vec![
        MenuItem::new("new", "New file"),
        MenuItem::new("open", "Open file"),
        MenuItem::new("save", "Save"),
    ]
};

Dropdown::uncontrolled(
    "dd-disabled-dd",
    Button::new("dropdown-disabled")
        .label("Actions")
        .variant(Variant::Secondary),
    plain(),
)
.id("dropdown-disabled-dd")
.disabled_keys([SharedString::from("save")])

With Sections

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Variant};

Dropdown::uncontrolled(
    "dd-sections-dd",
    Button::new("dropdown-sections")
        .label("Actions")
        .variant(Variant::Secondary),
    vec![
        MenuItem::SectionLabel("File".into()),
        MenuItem::new("new", "New file"),
        MenuItem::new("open", "Open file"),
        MenuItem::Separator,
        MenuItem::SectionLabel("Danger".into()),
        MenuItem::new("delete", "Delete").danger(),
    ],
)
.id("dropdown-sections-dd")

Controlled

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

let is_open = self.dropdown_open;
let selected = self
    .dropdown_selected
    .clone()
    .unwrap_or_else(|| SharedString::from("none"));
let plain = || {
    vec![
        MenuItem::new("new", "New file"),
        MenuItem::new("open", "Open file"),
        MenuItem::new("save", "Save"),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::new(
            "dropdown-controlled-dd",
            Button::new("dropdown-controlled")
                .label("Actions")
                .variant(Variant::Secondary),
            plain(),
            is_open,
        )
        .id("dropdown-controlled-dd")
        .on_open_change(cx.listener(|this, open: &bool, _, cx| {
            this.dropdown_open = *open;
            cx.notify();
        }))
        .on_action(cx.listener(|this, key: &SharedString, _, cx| {
            this.dropdown_selected = Some(key.clone());
            this.dropdown_open = false;
            cx.notify();
        }))
        .into_any_element(),
        para(&format!("Last action: {selected}"), cx),
    ])

Also calls para (included when you copy).

Controlled Open State

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, px};
use herogpui::gpui;

let is_open = self.dropdown_open;
let plain = || {
    vec![
        MenuItem::new("new", "New file"),
        MenuItem::new("open", "Open file"),
        MenuItem::new("save", "Save"),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        gpui::div()
            .flex()
            .flex_wrap()
            .items_center()
            .justify_center()
            .gap(px(12.))
            .children(vec![
                        Button::new("dropdown-open-btn")
                            .label(if is_open { "Close menu" } else { "Open menu" })
                            .size(Size::Sm)
                            .on_press(cx.listener(|this, _, _, cx| {
                                this.dropdown_open = !this.dropdown_open;
                                cx.notify();
                            }))
                            .into_any_element(),
                        para(if is_open { "Open" } else { "Closed" }, cx),
                    ])
            .into_any_element(),
        Dropdown::new(
            "dropdown-open-dd",
            Button::new("dropdown-open")
                .label("Actions")
                .variant(Variant::Secondary),
            plain(),
            is_open,
        )
        .id("dropdown-open-dd")
        .on_open_change(cx.listener(|this, open: &bool, _, cx| {
            this.dropdown_open = *open;
            cx.notify();
        }))
        .into_any_element(),
    ])

Also calls para (included when you copy).

With Single Selection

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, SelectionMode, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Dropdown::uncontrolled(
        "dd-single-dd",
        Button::new("dropdown-single")
            .label("Sort by")
            .variant(Variant::Secondary),
        vec![
            MenuItem::new("name", "Name"),
            MenuItem::new("date", "Date"),
            MenuItem::new("size", "Size"),
        ],
    )
    .id("dropdown-single-dd")
    .selection_mode(SelectionMode::Single)
    .default_selected_keys([SharedString::from("date")])
    .into_any_element()])

Single With Custom Indicator

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, IndicatorKind, MenuItem, SelectionMode, Variant};
use herogpui::gpui::SharedString;

Dropdown::uncontrolled(
    "dd-single-ind-dd",
    Button::new("dropdown-single-ind")
        .label("Sort by")
        .variant(Variant::Secondary),
    vec![
        MenuItem::new("name", "Name"),
        MenuItem::new("date", "Date"),
    ],
)
.id("dropdown-single-ind-dd")
.selection_mode(SelectionMode::Single)
.default_selected_keys([SharedString::from("name")])
.indicator(IndicatorKind::Dot)

Render Props

The composed Dropdown forwards item and indicator render state into the live menu. Open it and choose rows to watch the selection move. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, SelectionMode, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

let selected = self
    .dropdown_selected
    .clone()
    .unwrap_or_else(|| SharedString::from("none"));

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::uncontrolled(
            "dd-render-props-dd",
            Button::new("dropdown-render-props")
                .label("Render state")
                .variant(Variant::Secondary),
            vec![
                MenuItem::new("name", "Name"),
                MenuItem::new("date", "Date"),
                MenuItem::new("size", "Size"),
            ],
        )
        .selection_mode(SelectionMode::Multiple)
        .default_selected_keys([SharedString::from("name")])
        .item_content(|key, state| {
            gpui::div()
                .child(format!(
                    "{key}: {}",
                    if state.is_selected {
                        "selected"
                    } else {
                        "idle"
                    }
                ))
                .into_any_element()
        })
        .indicator_content(|_, selected, _| {
            gpui::div()
                .w(px(16.))
                .child(if selected { "✓" } else { "" })
                .into_any_element()
        })
        .into_any_element(),
    ])

With Section Level Selection

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, SelectionMode, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

let dd_multi = self.dropdown_multi.clone();

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Dropdown::uncontrolled(
        "dd-section-sel-dd",
        Button::new("dropdown-section-sel")
            .label("View")
            .variant(Variant::Secondary),
        vec![
            MenuItem::SectionLabel("Sort".into()),
            MenuItem::new("name", "Name"),
            MenuItem::new("date", "Date"),
            MenuItem::Separator,
            MenuItem::SectionLabel("Show".into()),
            MenuItem::new("hidden", "Hidden files"),
        ],
    )
    .id("dropdown-section-sel-dd")
    .selection_mode(SelectionMode::Multiple)
    .selected_keys(dd_multi)
    .on_selection_change(cx.listener(|this, keys: &[SharedString], _, cx| {
        this.dropdown_multi = keys.to_vec();
        cx.notify();
    }))
    .into_any_element()])

With Keyboard Shortcuts

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Dropdown::uncontrolled(
        "dd-shortcuts-dd",
        Button::new("dropdown-shortcuts")
            .label("Edit")
            .variant(Variant::Secondary),
        vec![
            MenuItem::new("cut", "Cut").shortcut("Ctrl X"),
            MenuItem::new("copy", "Copy").shortcut("Ctrl C"),
            MenuItem::new("paste", "Paste").shortcut("Ctrl V"),
        ],
    )
    .id("dropdown-shortcuts-dd")
    .into_any_element()])

With Submenus

Submenus position independently against their row end top and flip sides to the side with more room when needed; the parent stays anchored and outside presses still dismiss the whole menu. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Dropdown::uncontrolled(
        "dd-submenu-dd",
        Button::new("dropdown-submenu")
            .label("Share")
            .variant(Variant::Secondary),
        vec![
            MenuItem::new("link", "Copy link"),
            MenuItem::new("email", "Email"),
            MenuItem::new("other", "Other").submenu(vec![
                MenuItem::new("sms", "SMS"),
                MenuItem::new("airdrop", "AirDrop"),
                MenuItem::new("more", "More\u{2026}"),
            ]),
        ],
    )
    .id("dropdown-submenu-dd")
    .into_any_element()])

With Custom Submenu Indicator

`Dropdown.SubmenuIndicator` is the chevron on a row that opens another panel; hover the row to open it. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::uncontrolled(
            "dd-submenu-ind-dd",
            Button::new("dropdown-submenu-ind")
                .label("More")
                .variant(Variant::Secondary),
            vec![
                MenuItem::new("profile", "Profile"),
                MenuItem::new("workspace", "Workspace").submenu(vec![
                    MenuItem::new("members", "Members"),
                    MenuItem::new("billing", "Billing"),
                ]),
            ],
        )
        .id("dropdown-submenu-ind-dd")
        .into_any_element(),
    ])

Custom Trigger

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Avatar, Dropdown, MenuItem};

Dropdown::uncontrolled(
    "Jane Doe-dd",
    Avatar::new("dropdown-trigger-avatar").name("Jane Doe"),
    vec![
        MenuItem::new("profile", "Profile"),
        MenuItem::new("settings", "Settings"),
        MenuItem::Separator,
        MenuItem::new("logout", "Log out").danger(),
    ],
)
.id("Jane Doe-dd")

Long Press Trigger

Hold the button for half a second. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Button, Dropdown, DropdownTrigger, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let plain = || {
    vec![
        MenuItem::new("new", "New file"),
        MenuItem::new("open", "Open file"),
        MenuItem::new("save", "Save"),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::uncontrolled(
            "dd-long-dd",
            Button::new("dropdown-long")
                .label("Long press")
                .variant(Variant::Secondary),
            plain(),
        )
        .id("dropdown-long-dd")
        .trigger(DropdownTrigger::LongPress)
        .into_any_element(),
    ])

Basic Usage

The plainest action menu: `on_action` receives the chosen key, and a danger item renders in the danger palette. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::uncontrolled(
            "dd-basic-dd",
            Button::new("dropdown-basic")
                .label("Actions")
                .variant(Variant::Secondary),
            vec![
                MenuItem::new("new-file", "New file"),
                MenuItem::new("open-file", "Open file"),
                MenuItem::new("delete-file", "Delete file").danger(),
            ],
        )
        .id("dropdown-basic-dd")
        .on_action(cx.listener(|this, key: &SharedString, _, cx| {
            this.dropdown_last_basic = key.clone();
            cx.notify();
        }))
        .into_any_element(),
        para(
            &format!("Last action: {}", self.dropdown_last_basic),
            cx,
        ),
    ])

Also calls para (included when you copy).

Controlled Selection

The selected keys live in the caller and are fed back through `selected_keys`; the caption reads the same set the menu renders. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, SelectionMode, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::new(
            "dropdown-controlled-sel-dd",
            Button::new("dropdown-controlled-sel")
                .label("Formatting")
                .variant(Variant::Secondary),
            vec![
                MenuItem::new("bold", "Bold"),
                MenuItem::new("italic", "Italic"),
            ],
            self.dropdown_open,
        )
        .id("dropdown-controlled-sel-dd")
        .selection_mode(SelectionMode::Multiple)
        .selected_keys(self.dropdown_doc_marks.clone())
        .on_selection_change(cx.listener(|this, keys: &[SharedString], _, cx| {
            this.dropdown_doc_marks = keys.to_vec();
            cx.notify();
        }))
        .on_open_change(cx.listener(|this, open: &bool, _, cx| {
            this.dropdown_open = *open;
            cx.notify();
        }))
        .into_any_element(),
        para(
            &format!(
                "Active: {}",
                if self.dropdown_doc_marks.is_empty() {
                    "none".to_owned()
                } else {
                    self.dropdown_doc_marks
                        .iter()
                        .map(|k| k.as_ref())
                        .collect::<Vec<_>>()
                        .join(", ")
                }
            ),
            cx,
        ),
    ])

Also calls para (included when you copy).

With Multiple Selection

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, Dropdown, MenuItem, SelectionMode, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Dropdown::new(
            "dropdown-multi-dd",
            Button::new("dropdown-multi-trigger")
                .label("Columns")
                .variant(Variant::Secondary),
            vec![
                MenuItem::new("name", "Name"),
                MenuItem::new("role", "Role"),
                MenuItem::new("status", "Status"),
            ],
            self.dropdown_open,
        )
        .id("dropdown-multi-trigger-dd")
        .selection_mode(SelectionMode::Multiple)
        .selected_keys(self.dropdown_multi.clone())
        .disabled_keys(vec!["status"])
        .on_selection_change(cx.listener(|this, keys: &[SharedString], _, cx| {
            this.dropdown_multi = keys.to_vec();
            cx.notify();
        }))
        .on_open_change(cx.listener(|this, open: &bool, _, cx| {
            this.dropdown_open = *open;
            cx.notify();
        }))
        .into_any_element(),
        para(
            &format!("Showing {} columns", self.dropdown_multi.len()),
            cx,
        ),
    ])

Also calls para (included when you copy).

Context Menu

HeroGPUI extension, not a example: a secondary press opens at the pointer. Focus the Button and press Shift+F10 or the ContextMenu key to open below it; keyboard focus on unrecorded content falls back to the area's corner. The first item receives focus, and dismissal restores the previous focus. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Button, ContextMenu, Dropdown, MenuItem};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        ContextMenu::new(
            "dropdown-context",
            gpui::div()
                .w(px(320.))
                .h(px(140.))
                .flex()
                .flex_col()
                .items_center()
                .justify_center()
                .gap(px(12.))
                .rounded(px(12.))
                .border_1()
                .border_color(cx.colors().border)
                .text_color(cx.colors().muted)
                .child("Right-click anywhere in this area")
                .child(Button::new("dropdown-context-focus").label("Focus and press Shift+F10")),
            vec![
                MenuItem::new("cut", "Cut").shortcut("Ctrl X"),
                MenuItem::new("copy", "Copy").shortcut("Ctrl C"),
                MenuItem::new("paste", "Paste").shortcut("Ctrl V"),
                MenuItem::Separator,
                MenuItem::new("delete", "Delete").danger(),
            ],
        )
        .disabled_keys(["paste"])
        .on_action(cx.listener(|this, key: &SharedString, _, cx| {
            this.context_menu_last = key.clone();
            cx.notify();
        }))
        .into_any_element(),
        para(&format!("Last action: {}", self.context_menu_last), cx),
    ])

Also calls para (included when you copy).

Menu Bar

HeroGPUI extension, not a example: `MenuBar` is a horizontal bar of the same menu panels. Left and Right rove between the top-level items (skipping the disabled Help), Enter, Space or Down opens one with its first item focused, and while a menu is open Left, Right or hovering another item switches menus. Escape closes it and returns the focus to its item. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, Dropdown, MenuBar, MenuBarMenu, MenuItem};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        MenuBar::new(
            "dropdown-menu-bar-bar",
            vec![
                MenuBarMenu::new("file", "File", vec![
                    MenuItem::new("new", "New").shortcut("Ctrl N"),
                    MenuItem::new("open", "Open").shortcut("Ctrl O"),
                    MenuItem::Separator,
                    MenuItem::new("quit", "Quit").danger(),
                ]),
                MenuBarMenu::new("edit", "Edit", vec![
                    MenuItem::new("undo", "Undo").shortcut("Ctrl Z"),
                    MenuItem::new("redo", "Redo").shortcut("Ctrl Y"),
                ])
                .disabled_keys(["redo"]),
                MenuBarMenu::new("view", "View", vec![
                    MenuItem::new("zoom-in", "Zoom In"),
                    MenuItem::new("zoom-out", "Zoom Out"),
                ]),
                MenuBarMenu::new("help", "Help", vec![MenuItem::new("about", "About")])
                    .is_disabled(true),
            ],
        )
        .on_action({
            let view = cx.entity().downgrade();
            move |menu, item, _, cx| {
                view.update(cx, |this, cx| {
                    this.menu_bar_last = SharedString::from(format!("{menu} / {item}"));
                    cx.notify();
                })
                .ok();
            }
        })
        .into_any_element(),
        para(&format!("Last action: {}", self.menu_bar_last), cx),
    ])

Also calls para (included when you copy).

Helpers used by these examples

Plain functions over the public API, defined once here and copied with each example that calls them.

para
fn para(text: &str, cx: &App) -> gpui::AnyElement {
    let colors = cx.colors();
    gpui::div()
        .text_size(px(14.5))
        .line_height(px(24.))
        .text_color(colors.foreground)
        .max_w(px(720.))
        .child(text.to_owned())
        .into_any_element()
}

Anatomy

Rust types

DropdownMenuMenuItem

Dropdown is assembled from these builders. The API reference lists each one.

Customization

Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Dropdown looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.

Styling

Dropdown styling
MethodValuesDescription
placement(DropdownPlacement)PopoverPlacementPlacement relative to the measured trigger. All 22 union placements flip to the side with more room when the preferred side cannot fit, keeping a 12px cross-axis viewport inset with the scroller capped to the available height.
Rust
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
    el.bg(gpui::rgba(0xffa500ff))             // background
        .text_color(gpui::rgba(0x000000ff))   // text
        .w(gpui::px(13.))                     // size
        .h(gpui::px(12.))
})

API reference

Against HeroUI v3.2.6: 36 entries ported, 11 partial, 14 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Dropdown builders
BuilderValuesDefaultDescription
is_open(bool)true | false—Sets the open state of the menu (controlled).
default_open(bool)true | false—Sets the default open state of the menu (uncontrolled).
on_open_change(callback)Fn(isOpen: bool) -> ()—Handler called when the open state changes.
trigger(DropdownTrigger)DropdownTrigger::Press | DropdownTrigger::LongPressDropdownTrigger::PressThe type of interaction that triggers the menu.
new(id, trigger, items, is_open)PartialAnyElement—Dropdown content.
placement(DropdownPlacement)PopoverPlacementDropdownPlacement::BottomPlacement relative to the measured trigger. All 22 union placements flip to the side with more room when the preferred side cannot fit, keeping a 12px cross-axis viewport inset with the scroller capped to the available height.
selection_mode(SelectionMode)SelectionMode::Single | SelectionMode::Multiple | SelectionMode::NoneSelectionMode::NoneWhether single or multiple selection is enabled.
selected_keys(keys)iterable of keys—The currently selected keys (controlled).
default_selected_keys(keys)iterable of keys—The initial selected keys (uncontrolled).
on_selection_change(callback)Fn(keys: Selection) -> ()—Handler called when the selection changes.
disabled_keys(keys)iterable of keys—Keys of disabled items.
on_action(callback)Fn(key: Key) -> ()—Handler called when an item is activated.
new(id, items)AnyElement—Menu content.
SectionLabel + itemsPartialAnyElement—Section content.
new(key, label)Key—Unique identifier for the item.
danger()Partial'default' | 'danger'"default"Visual variant of the item.
item_content(render)AnyElement | render closure—Item content or a render function.
indicator(IndicatorKind)IndicatorKind::Checkmark | IndicatorKind::DotIndicatorKind::CheckmarkType of selection indicator to display.
indicator_content(render)AnyElement | render closure—Custom indicator content or render function.
item_content(state.is_selected)true | false—Render-function state: whether the item is selected.
item_content(state.is_focused)true | false—Render-function state: whether the item has focus.
item_content(state.is_disabled)true | false—Render-function state: whether the item is disabled.
item_content(state.is_pressed)true | false—Render-function state: whether the item is being pressed.
submenu(items)PartialAnyElement—Custom submenu indicator content.
Not ported14 HeroUI entries not ported
Dropdown builders not ported
HeroUI v3Why it is not ported
Dropdown.className
Dropdown.Trigger.classNameFor the trigger.
Dropdown.Popover.classNameFor the popover.
Dropdown.Menu.classNameFor the menu. Standalone Menu provides panel sizing, row metrics, panel_gap and panel_padding builders; sx styles its outer wrapper.
Dropdown.Section.selectionModeSelection mode for items within this section.
Dropdown.Section.selectedKeysThe currently selected keys (controlled).
Dropdown.Section.defaultSelectedKeysThe initial selected keys (uncontrolled).
Dropdown.Section.onSelectionChangeHandler called when section selection changes.
Dropdown.Section.disabledKeysKeys of disabled items in the section.
Dropdown.Section.classNameFor the section.
Dropdown.Item.classNameFor the item.
Dropdown.ItemIndicator.classNameFor the indicator.
Dropdown.SubmenuIndicator.classNameFor the submenu indicator.
Dropdown.SubmenuTrigger.classNameFor the submenu trigger.

Parts

Dropdown parts
PartDescription
DropdownMenuTrigger wrapper that owns open state and composes the trigger and popover.
DropdownButton or custom element that opens the menu.
DropdownFloating panel positioned relative to the trigger.
MenuScrollable menu container and selection owner.
MenuPartialGrouped menu items with an optional header.
MenuItemFocusable action or selectable menu row; descriptions wrap within the popover width.
MenuCheckmark or dot selection marker.
MenuItemPartialTrailing chevron for an item that opens a submenu.
MenuItemPartialItem wrapper that keeps the parent open while opening a child popover.

States

Dropdown states
StateBuilderDescription
Hoveredtrack_interactionPointer is over the trigger or a menu item.
FocusedFocusHandleElement owns keyboard focus.
Focus visiblefocus_handleKeyboard focus indicator is visible.
Pressedtrack_interactionTrigger or item is being activated.
Disableddisabled_keysTrigger or item cannot be activated.
Selectedselected_keysItem is selected in single or multiple mode.
Enteringoverlay_phase::EnteringPopover is playing its enter transition.
Exitingoverlay_phase::ExitingPopover remains mounted while its exit transition plays.