limn
    ↑↓ ChooseEnter OpenEsc Close
    GitHub

    Patterns/Overlays

    Catalog menus

    A menu that lists more than actions: a selector with a searchable catalog, folding sections and two-line rows. There is no Picker type; it is a DropdownMenu with a MenuHeader, DropdownMenuSections and items that have a description. MenuItem is the same row on its own, and PopoverPanel the layout of a Popover that is not a menu.

    Composed patternGuide source

    Preview

    CodeSource
    fn view(state: &State, palette: ComponentPalette) -> Element {
        let width = SelectorSizing::CATALOG.preferred_width;
        let entries = state
            .models_matching(&state.query)
            .map(|model| {
                DropdownMenuItem::new(format!("model={}", model.id), &model.name)
                    .description(&model.id)
                    .detail_mono(true)
                    .selected(model.id == state.model)
                    .into()
            })
            .collect();
        DropdownMenu::new(
            Button::new("models", "Model", palette).render(),
            entries,
            state.models_open,
            "models:dismiss",
            palette,
        )
        .header(
            MenuHeader::new(palette)
                .title("Model")
                .search("models:search", &state.query, "Search models")
                .width(width - 8.0)
                .render(),
        )
        .context_menu_style()
        .width(width)
        .max_visible_items(6)
        .render()
    }
    
    fn update(state: &mut State, action: &UiAction) {
        match action {
            UiAction::InputChanged { id, value } if id == "models:search" => state.query = value.clone(),
            UiAction::Clicked { id } if id == "models" => state.models_open = true,
            UiAction::Clicked { id } if id == "models:dismiss" => state.models_open = false,
            UiAction::Clicked { id } => {
                if let Some(model) = id.strip_prefix("model=") {
                    state.choose_model(model);
                }
            }
            _ => {}
        }
    }

    Examples

    Each one is the library drawing it. Press, type and hover.

    Select and catalog2 / 2
    CodeSource
    row(vec![actions(cx)])
    Menu rows
    CodeSource
    row(vec![rows(cx)])
    Popover panel
    CodeSource
    row(vec![panel(cx, false), panel(cx, true)])

    Catalog menus

    A menu that lists more than actions: a selector with a searchable catalog, folding sections and two-line rows. There is no Picker type; it is a DropdownMenu with a MenuHeader, DropdownMenuSections and items that have a description. MenuItem is the same row on its own, and PopoverPanel the layout of a Popover that is not a menu.

    Import

    use limn::{
        Component, DropdownMenu, DropdownMenuEntry, DropdownMenuItem, DropdownMenuSection,
        MenuHeader, MenuItem, PopoverPanel, SelectorSizing,
    };

    Usage

    The menu wraps its trigger where the trigger is built, and the host owns the open flag, the query and the choice. An item’s ID is the action it reports.

    fn view(state: &State, palette: ComponentPalette) -> Element {
        let width = SelectorSizing::CATALOG.preferred_width;
        let entries = state
            .models_matching(&state.query)
            .map(|model| {
                DropdownMenuItem::new(format!("model={}", model.id), &model.name)
                    .description(&model.id)
                    .detail_mono(true)
                    .selected(model.id == state.model)
                    .into()
            })
            .collect();
        DropdownMenu::new(
            Button::new("models", "Model", palette).render(),
            entries,
            state.models_open,
            "models:dismiss",
            palette,
        )
        .header(
            MenuHeader::new(palette)
                .title("Model")
                .search("models:search", &state.query, "Search models")
                .width(width - 8.0)
                .render(),
        )
        .context_menu_style()
        .width(width)
        .max_visible_items(6)
        .render()
    }
    
    fn update(state: &mut State, action: &UiAction) {
        match action {
            UiAction::InputChanged { id, value } if id == "models:search" => state.query = value.clone(),
            UiAction::Clicked { id } if id == "models" => state.models_open = true,
            UiAction::Clicked { id } if id == "models:dismiss" => state.models_open = false,
            UiAction::Clicked { id } => {
                if let Some(model) = id.strip_prefix("model=") {
                    state.choose_model(model);
                }
            }
            _ => {}
        }
    }

    The host filters the entries itself: the menu shows what it is given.

    Two-line items

    DropdownMenuItem::new("model=north/large-2", "North Large")
        .icon(Icon::Server)
        .description("north/large-2")
        .detail_mono(true)
        .status("Reviewed")
        .status_width(84.0)
        .selected(true)
        .into()

    With description an item is a two-line row, SelectorSizing::detail_row_height (40) tall, with a column for the check. It takes the menu’s sizing and the sides, icon slot, label size and gap of its one-line rows, so a menu with a profile of its own gets two-line rows that line up with its other rows; with SelectorSizing::COMPACT or CATALOG the row is as it always was. detail_mono sets the second line in monospace, status puts short muted text at its end and status_width fixes that slot so the statuses start in a column. chevron() ends the row with a chevron instead of the check, for a row that opens another view; with a submenu the row ends with a chevron and opens it. A two-line item ignores shortcut, annotation and destructive.

    truncate() cuts a long label to one line in a menu with context_menu_style(), where it would otherwise wrap; pair it with tooltip_with to say the rest.

    MenuHeader::new(palette)
        .back("models:back", "Model")
        .trailing(refresh_button)
        .search("models:search", &state.query, "Search models")
        .render()

    DropdownMenu::header keeps an element above the options while they scroll, and footer keeps one under them, below a hairline. MenuHeader is the usual header: a title, or a back button that reports its ID, one square trailing tool and a search field under the row. width fixes its width.

    Sections

    DropdownMenuSection::new("fold:north", "North")
        .icon(Icon::Server)
        .count(2)
        .collapsed(state.north_folded)
        .into()

    A section is a heading that folds. It reports its ID when activated; the host flips its own flag and leaves the group’s items out of the entries while the section is collapsed.

    Loading, empty and error

    DropdownMenuEntry::Loading { id: "branches:loading".into(), rows: 3 }
    DropdownMenuEntry::Message("No branches yet.".into())
    DropdownMenuEntry::Error { id: "branches:error".into(), text: error.to_string() }

    Loading shows skeleton rows, Message a muted sentence and Error a failure in a short scroll area of its own.

    Shared sizes

    SelectorSizing::COMPACT (256 wide, 8 rows before scrolling) is for short lists of actions and SelectorSizing::CATALOG (320 wide, 6 rows) for searchable catalogs. Read a field such as preferred_width, or pass the profile to DropdownMenu::sizing.

    Rows on their own

    MenuItem::new("terminal=1", "Terminal 1", palette)
        .icon(Icon::Terminal)
        .trailing("Active")
        .trailing_action("terminal-1-close", Icon::Trash, TooltipSpec::new("Close terminal"))
        .trailing_dispatch_as("terminal:close=1")
        .width(300.0)
        .render()

    MenuItem is a menu row for a list the host lays out itself. It takes icon, trailing text or a shortcut, destructive, tooltip_with, and the two-line options above: description, detail_mono, status, status_width, chevron and selected. It is 220 wide unless width is set. enabled(false) makes it passive. trailing_action adds a second square action at the end of the row, reached by Tab and not by the arrow keys; trailing_dispatch_as makes it report another ID.

    A panel in a popover

    Popover::new(
        trigger,
        PopoverPanel::new(palette)
            .header(PanelHeader::new("Account", palette).dismiss("account:dismiss", "Close").render())
            .body(ScrollArea::new("account:body", rows).render())
            .body_max_height(180.0)
            .footer(Button::new("account:details", "See usage", palette).render())
            .render(),
        state.account_open,
        "account:dismiss",
        palette,
    )
    .render()

    PopoverPanel stacks a header, a body and a footer with hairlines between them. The body gives way first in a short window; body_max_height caps it, for a body that is a scroll area. sizing(PopoverPanelSizing { gap, padding, header_rule }) sets another rhythm; PopoverPanelSizing::DEFAULT is 4, 0 and a hairline under the header. The popover keeps placement, width and surface.

    Actions

    ActionWhen
    UiAction::Clicked { id } with an item’s or a row’s IDThe item is activated.
    UiAction::Clicked { id } with a section’s IDIts heading is activated.
    UiAction::Clicked { id } with the back IDThe header’s back button.
    UiAction::InputChanged { id, value } with the search IDThe query changes.
    UiAction::Clicked { id } with the dismiss IDEscape, or a click outside the menu.

    Accessibility

    A MenuItem is a menu item named by its label. Its trailing text or its description is said after the name, a two-line row given selected says whether it is, and a disabled one is there and cannot be used. A trailing action is a button named by the title of its hint.

    API reference

    The guide covers composition, options and actions. Generate the complete Rust type and builder reference from the checkout:

    cargo doc -p limn --open

    Component API overview · State and actions

    The example is drawn by crates/limn-gallery/src/pages/menus.rs.