limn
    ↑↓ ChooseEnter OpenEsc Close
    GitHub

    Components/Data

    ListRow

    One entry of a list: an icon, a title, an optional second line and what sits at its trailing edge, all under one hover surface.

    Native componentGuide source

    Preview

    CodeSource
    fn view(state: &State, palette: ComponentPalette) -> Element {
        Element::column(
            state
                .files
                .iter()
                .map(|file| ListRow::new(format!("file:{}", file.name), &file.name, palette).icon(Icon::File).render())
                .collect(),
        )
        .gap(2.0)
    }
    
    fn update(state: &mut State, action: &UiAction) {
        if let UiAction::Clicked { id } = action {
            if let Some(name) = id.strip_prefix("file:") {
                state.open(name);
            }
        }
    }

    Examples

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

    Compact
    CodeSource
    card(cx, width, vec![
                ListRow::new("list-branch", "ui-separation", on_card)
                    .icon(Icon::GitBranch)
                    .trailing_icon(Icon::ChevronDown)
                    .tooltip_with(hint("Switch branch"))
                    .width(inner)
                    .render(),
                ListRow::new("list-changes", "12 files", on_card)
                    .icon(Icon::Code)
                    .trailing(stat())
                    .width(inner)
                    .render(),
                ListRow::new("list-source", "list_row.rs", on_card)
                    .icon(Icon::File)
                    .subtitle("crates/limn/src/list_row.rs")
                    .subtitle_mono(true)
                    .meta("2 min ago")
                    .trailing_icon(Icon::ChevronRight)
                    .width(inner)
                    .render(),
                ListRow::new("list-long", "A title too long to fit the width of this list", on_card)
                    .icon(Icon::Book)
                    .subtitle("And a second line that also overflows and ends with an ellipsis")
                    .meta_element(stat())
                    .width(inner)
                    .render(),
                ListRow::new("list-locked", "Locked while saving", on_card)
                    .icon(Icon::Folder)
                    .subtitle("No click target or focus")
                    .trailing_icon(Icon::ChevronDown)
                    .enabled(false)
                    .width(inner)
                    .render(),
            ])
    Dense
    CodeSource
    card(
                cx,
                width,
                FRAMES
                    .iter()
                    .enumerate()
                    .map(|(index, (name, place))| {
                        ListRow::new(format!("list-frame-{index}"), *name, on_card)
                            .icon(Icon::Terminal)
                            .meta(*place)
                            .sizing(ListRowSizing::DENSE)
                            .width(inner)
                            .render()
                    })
                    .collect(),
            )
    Leading element, filled, with no fixed width
    CodeSource
    Element::row(vec![
        ListRow::new("list-run", "Run the project", cx.palette)
            .leading(Element::spacer().background(cx.palette.selected).radius(limn::RadiusRole::Control.px()))
            .subtitle("cargo run -p limn-gallery")
            .subtitle_mono(true)
            .filled()
            .render()
            .flex(1.0),
        Button::new("list-run-edit", "Edit", cx.palette)
            .variant(ButtonVariant::Ghost)
            .size(ControlSize::Compact)
            .render(),
    ])
    .gap(8.0)
    .size(Some(width), None)
    Subtitle color, outline, and fixed text width
    CodeSource
    ListRow::new("list-account", "Account and usage", cx.palette)
        .icon(Icon::Chat)
        .subtitle("Provider · Not updated")
        .subtitle_ink(colors(AlertTone::Warning))
        .trailing_icon(Icon::ChevronRight)
        .filled()
        .width(width)
        .render(),
    ListRow::new("list-citation", "limn documentation", cx.palette)
        .icon(Icon::Search)
        .subtitle("https://example.com/limn/list-row")
        .subtitle_mono(true)
        .filled()
        .outlined()
        .width(width)
        .render(),
    ListRow::new("list-budget", "A title cut to the 160 px reserved by the row", on_card)
        .icon(Icon::Folder)
        .subtitle("The rest of the row remains available")
        .trailing_icon(Icon::ChevronDown)
        .copy_width(160.0)
        .filled()
        .width(width)
        .render()
    Sidebar
    CodeSource
    card(cx, width, vec![
        tree_header(cx, on_card, "list-tree-open", "limn", 3, true, inner),
        task(cx, on_card, "list-task-a", "Split the application view into components", false, inner),
        task(cx, selected(cx), "list-task-b", "Sidebar rows", true, inner),
        tree_header(cx, on_card, "list-tree-folded", "documents", 128, false, inner),
    ])
    Separate actions sharing one hover
    CodeSource
    card(cx, width, vec![
        change(cx, on_card, "list-change-a", "list_row.rs", "M", true, inner),
        change(cx, on_card, "list-change-b", "tree_view.rs", "A", true, inner),
        change(cx, on_card, "list-change-c", "locked.rs", "D", false, inner),
    ])
    Change counts and path tooltip
    CodeSource
    Element::row(vec![
                stat(),
                DiffStat::new(3, 0, cx.palette).render(),
                Tooltip::for_path(
                    Button::new("list-path", "tree.rs", cx.palette).variant(ButtonVariant::Ghost).render(),
                    "list-path",
                    "crates/limn/src/tree.rs",
                    cx.palette,
                )
                .placement(TooltipPlacement::Right)
                .render(),
                Element::spacer().flex(1.0),
            ])
            .gap(16.0)
    Bring a row into view
    CodeSource
    Element::row(vec![
        // The gallery's host calls `reveal` with what follows the prefix.
        Button::new("reveal:list-reveal-17", "Show row 18", cx.palette).variant(ButtonVariant::Subtle).render(),
        Button::new("reveal:list-reveal-0", "Back to the first", cx.palette).variant(ButtonVariant::Ghost).render(),
        Element::spacer().flex(1.0),
    ])
    .gap(8.0),
    card(cx, width, vec![ScrollArea::new(
        "list-reveal",
        Element::column(
            (0..24)
                .map(|index| {
                    let row = ListRow::new(format!("list-reveal-{index}"), format!("Row {}", index + 1), on_card);
                    if index % 3 == 0 { row.subtitle("With a second line") } else { row }.render()
                })
                .collect(),
        )
        .gap(2.0),
    )
    .render()
    .size(None, Some(150.0))])
    Sticky header while scrolling
    CodeSource
    card(cx, width, vec![ScrollArea::new(
        "list-sticky",
        Element::column(
            (0..3)
                .flat_map(|group| {
                    let title = if group == 1 {
                        "Group 2, with a long header that spans several lines in place and shrinks while pinned to the top".to_owned()
                    } else {
                        format!("Group {}", group + 1)
                    };
                    let header = Element::panel(limn::Text::new(title, on_card).render())
                        .padding(8.0)
                        .background(cx.palette.surface)
                        .radius(limn::RadiusRole::Control.px())
                        .sticky_top_max_height(36.0)
                        .sticky_top_fade(16.0);
                    std::iter::once(header).chain((0..8).map(move |index| {
                        ListRow::new(format!("list-sticky-{group}-{index}"), format!("Row {}", index + 1), on_card).render()
                    }))
                })
                .collect(),
        )
        .gap(2.0),
    )
    .render()
    .top_fade(cx.palette.raised, 6.0)
    .size(None, Some(180.0))])

    ListRow

    One entry of a list: an icon, a title, an optional second line and what sits at its trailing edge, all under one hover surface.

    Import

    use limn::{Component, DiffStat, Icon, ListRow, RadiusRole, ListRowSizing, TooltipSpec};

    Usage

    A row needs an ID, a title and the palette. It reports UiAction::Clicked with its ID; the host owns the selection and the action.

    fn view(state: &State, palette: ComponentPalette) -> Element {
        Element::column(
            state
                .files
                .iter()
                .map(|file| ListRow::new(format!("file:{}", file.name), &file.name, palette).icon(Icon::File).render())
                .collect(),
        )
        .gap(2.0)
    }
    
    fn update(state: &mut State, action: &UiAction) {
        if let UiAction::Clicked { id } = action {
            if let Some(name) = id.strip_prefix("file:") {
                state.open(name);
            }
        }
    }

    A row has no fill at rest. It hovers to the palette’s hover and presses to its selected, so a row on a card takes a palette whose hover is one step above that card.

    Icon and leading element

    ListRow::new("branch", "main", palette).icon(Icon::GitBranch).render()
    ListRow::new("run", "Run the project", palette).leading(thumbnail).render()

    icon is a muted glyph before the copy, centered in the row’s height; icon_ink paints it in another color. leading takes an element of the host’s, such as a thumbnail, held in a square at the top of the row. It replaces the icon.

    Subtitle

    ListRow::new("source", "list_row.rs", palette)
        .subtitle("crates/limn/src/list_row.rs")
        .subtitle_mono(true)
        .render()

    subtitle adds a muted second line; an empty one is no line at all. subtitle_mono sets it in the monospaced family, for a command or a path. subtitle_ink paints it in a color that carries meaning, such as a warning.

    Meta

    ListRow::new("source", "list_row.rs", palette).meta("2 min ago").render()
    ListRow::new("changes", "12 files", palette)
        .meta_element(DiffStat::new(128, 17, palette).render())
        .render()

    meta is muted text at the end of the title’s line, such as a status or an age. meta_element puts an element of the host’s in the same place; each call adds one more, after the meta text.

    Trailing

    ListRow::new("branch", "main", palette).trailing_icon(Icon::ChevronDown).render()
    ListRow::new("changes", "12 files", palette).trailing(stat).render()

    trailing is an element after the copy, across the row’s height. trailing_icon is a muted glyph at the trailing edge, such as the chevron of a row that opens a menu.

    Controls inside a row

    ListRow::new("task", "Split the view", palette)
        .trailing(more_button)
        .hover_with("task-more")
        .render()
    
    ListRow::new("change", "list_row.rs", palette)
        .trailing_action("change-stage", stage_control)
        .render()

    hover_with(id) keeps the row hovered while the pointer is over the control id, for a trailing element that takes the pointer from the row.

    trailing_action(id, control) adds a control with a hit target of its own after the row’s; each call adds one more. The row and its actions then share one hover surface, painted behind all of them, so draw the control with no hover fill of its own. The row’s hint covers the row’s own target alone.

    Title

    ListRow::new("task", state.title(), palette).title_fade(true).render()
    ListRow::new("mail", "Unread", palette).title_ink(accent).render()

    A title that does not fit is cut with an ellipsis. title_fade(true) fades its end instead, for a title that changes while it is on screen. title_ink paints it in a color the host owns; it holds while the row is disabled.

    Fill, outline and radius

    ListRow::new("account", "Account", palette).filled().outlined().render()
    ListRow::new("task", "Sidebar rows", palette).radius(RadiusRole::Surface).render()

    filled fills the row with the palette’s surface at rest, for a row that stands on its own. outlined draws a hairline in the palette’s border color, for a row that is a card of its own. radius takes the role of the corners of the fill and the hover; RadiusRole::Control unless set.

    Width

    ListRow::new("source", "list_row.rs", palette).width(320.0).render()
    ListRow::new("budget", "A long title", palette).copy_width(160.0).render()

    Without width the row takes the width it is given. width fixes it and clips the content to it. copy_width holds the title and subtitle at a fixed width instead of the width the row has left.

    Sizing

    ListRow::new("frame", "render", palette).sizing(ListRowSizing::DENSE).render()

    ListRowSizing::COMPACT, the default, is 28 high, or 40 with a subtitle. ListRowSizing::DENSE is 24 and 36, for a long list of short entries. ListRowSizing is a struct of the heights, padding, gaps, column widths and font sizes, so a host can write a profile of its own; a None height follows the content.

    Disabled

    ListRow::new("locked", "Locked while saving", palette).enabled(false).render()

    A disabled row is passive: a muted title and no hit region.

    Hint

    ListRow::new("branch", "main", palette)
        .tooltip_with(TooltipSpec::new("Switch branch"))
        .render()

    The hint is shown over the row, enabled or not.

    Actions

    ActionWhen
    UiAction::Clicked { id } with the row’s IDAn enabled row is activated.
    UiAction::Clicked { id } with a trailing action’s IDThe control given to trailing_action is activated, if the host built it as a click target with that ID.

    Accessibility

    A row is an item of a list. One that can be clicked is named by everything it shows; a disabled one is named by its title, with its subtitle as a description, and says it cannot be used. The row keeps no selection of its own, so the host says which one is chosen:

    ListRow::new("inbox", "Inbox", palette).render().accessible(Access::default().selected(true))

    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/lists.rs.