limn
    ↑↓ ChooseEnter OpenEsc Close
    GitHub

    Components/Layout

    ScrollArea

    A viewport that scrolls content taller than itself, with a vertical scrollbar. It also builds long lists, of rows of one height or of rows that each take their own, only the rows in view.

    Native componentGuide source

    Preview

    CodeSource
    fn view(state: &State, palette: ComponentPalette) -> Element {
        ScrollArea::new("files", Element::column(rows(state, palette)))
            .render()
            .size(None, Some(240.0))
    }

    Examples

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

    Scroll area2 / 7
    CodeSource

    Read the source of this example.

    Scroll area3 / 7
    CodeSource

    Read the source of this example.

    Scroll area4 / 7
    CodeSource

    Read the source of this example.

    Scroll area5 / 7
    CodeSource

    Read the source of this example.

    Scroll area6 / 7
    CodeSource

    Read the source of this example.

    Scroll area7 / 7
    CodeSource

    Read the source of this example.

    Column height budgets1 / 3
    CodeSource
    column_height_budget(cx, width, 240.0, false, "short")
    Column height budgets2 / 3
    CodeSource
    column_height_budget(cx, width, 300.0, true, "tall")
    Column height budgets3 / 3
    CodeSource
    column_height_budget(cx, width, 180.0, true, "tight")

    ScrollArea

    A viewport that scrolls content taller than itself, with a vertical scrollbar. It also builds long lists, of rows of one height or of rows that each take their own, only the rows in view.

    Import

    use limn::{Component, ScrollArea, ScrollMotion, ScrollbarAppearance, ScrollbarVisibility};

    Usage

    A scroll area takes an ID and its content. Give the rendered element a height, or flex in a column, and the content scrolls inside it. The ID keeps the scroll position from one frame to the next, so it must stay the same.

    fn view(state: &State, palette: ComponentPalette) -> Element {
        ScrollArea::new("files", Element::column(rows(state, palette)))
            .render()
            .size(None, Some(240.0))
    }

    It reports no actions. To scroll it from the host see Layout, and to keep a child pinned at its top see Layout.

    Give the rendered scroll area a max_height to keep short content at its natural height. Mark its direct column child with shrink_height_to_fit() to yield more height when the footer needs it:

    Element::column(vec![
        body.flex(1.0),
        ScrollArea::new("notices", notices)
            .render()
            .max_height(160.0)
            .shrink_height_to_fit(),
        footer,
    ]).gap(8.0)

    If a row wraps the scroll area, mark that row instead. Its actual assigned height bounds the viewport, so the last row remains reachable by scrolling. The flag does not change row widths or columns that do not use it. See Layout for minimums and the limits of an already overfilled column.

    A list of uniform rows

    ScrollArea::uniform_list("log", state.lines.len(), 28.0, move |index| {
        ListRow::new(format!("line-{index}"), format!("Line {}", index + 1), palette).render()
    })
    .render()
    .flex(1.0)

    uniform_list(id, row_count, row_height, build_row) builds each row from its index when it comes into view, so the row count can be large. The closure is Fn(usize) -> Element + Send + Sync + 'static: it owns what it reads.

    ScrollArea::uniform_list("log", count, 28.0, build_row).expansion(state.open_row, details)

    expansion(index, content) shows content under one row, at the height it takes. It scrolls with the rows and the rows after it move down. One row is open at a time; pass the content on every rebuild while it is.

    A list of rows of their own heights

    ScrollArea::measured_list(
        "messages",
        state.messages.len(),
        56.0,
        move |index| ids[index],
        move |index| message_row(&messages[index], palette),
    )
    .follow_end(true)
    .render()
    .flex(1.0)

    measured_list(id, row_count, estimated_row_height, row_key, build_row) is a list of rows that each take the height they need, such as the messages of a conversation. Like a uniform list it builds only the rows in view. Each row is measured at the width of the list when it is built; until then estimated_row_height stands for it, so the scrollbar is exact for the rows seen and close for the others.

    row_key names the row at an index, with a u64 that stays the same for that row: an ID the host already has, or a hash of one. The tree keeps each height under its key, so a row measured once is not measured from nothing again, and the row at the top of the viewport stays where it is when the rows above it are measured, grow, shrink or are added. A page of older messages loaded above the ones being read does not move them, nor does a row above them that a reaction made taller.

    follow_end(true) keeps the list at its end while it is scrolled there: rows added at the end, or a last row that grows, come into view, as a conversation follows new messages. Scrolled away from the end, the list holds its place. A short list stays at its top until it overflows.

    UiTree::visible_rows(id) gives the range of rows built at the last layout, for a host that loads more before the first one shows or marks what was seen. scroll_to_index, scroll_to_end and Tab work as they do in a uniform list, and reveal and anchor_offset reach controls and anchors inside the rows in view.

    A measured list has no expansion: a row that opens grows itself.

    Scrollbar

    ScrollArea::new("files", content).appearance(ScrollbarAppearance::overlay()).render()

    By default the scrollbar has a rail of its own beside the content while the content overflows, so it stays clear of controls at the content’s edge. ScrollbarAppearance::overlay() draws it over the content, which keeps its width. Content that fits shows no scrollbar and reserves no rail.

    FieldDefaultWhat it is
    placementScrollbarPlacement::ReservedBeside the content, or Overlay on it
    buttonstrueAn up and a down button at the ends of the rail
    rail_width14From 4 to 24 through .rail_width
    thumb_width5From 2 to 12 through .thumb_width
    thumb_radiusNone (round ends)The corner of the thumb and its track; .thumb_radius(0.0) draws them square. Never past half the thumb’s width
    min_thumb_length24The shortest the thumb gets
    tintNoneThe scrollbar’s color; None uses the element’s foreground
    idle_thumb_opacity, hover_thumb_opacity, active_thumb_opacity72, 118, 164The thumb at rest, under the pointer and dragged, out of 255
    hover_track_opacity14The rail under the pointer; at rest it is transparent

    ScrollArea::buttons(enabled) and ScrollArea::tint(color) change that one field and keep the rest of the appearance.

    To show a scrollbar only after scrolling, opt in to activity visibility:

    ScrollArea::new("files", content)
        .appearance(ScrollbarAppearance::overlay().buttons(false))
        .visibility(ScrollbarVisibility::AutoHide { hold_ms: 600, fade_ms: 200 })
        .render()

    The bar starts hidden. Changes in scroll position show it immediately; hovering or dragging a visible bar keeps it shown. After activity stops it stays for hold_ms, then fades over fade_ms. A hidden bar has no pointer target. Hiding preserves content width, including any reserved rail, so rows never move sideways. UiTree::animation_repaint_after() includes its hold deadline and fade; reduced motion keeps the hold and hides without fading. ScrollbarVisibility::Always is the unchanged default. The policy also works on uniform and measured lists, and on low-level scroll elements through Element::scrollbar_visibility.

    ScrollbarVisibility::AutoHideUnlessHovered { hold_ms, fade_ms } also shows the bar while the pointer is over the scroll surface, as a chat’s history shows its bar under the pointer, and starts the hold when the pointer leaves. The surface’s visible box decides, whatever lies over it.

    The same policy applies to the thin indicator enabled by drag_to_pan() on Element::horizontal_scroll and horizontal_scroll_2d:

    Element::horizontal_scroll_2d("shelf", cards)
        .drag_to_pan()
        .scrollbar_visibility(ScrollbarVisibility::AutoHide { hold_ms: 600, fade_ms: 200 })

    A press anywhere on the viewport pans it, on a button inside too once the pointer travels 4 px sideways, and the button is then not clicked; a press that does not travel still clicks it. A drag let go with speed glides on and slows to rest, unless motion is reduced. Carousel is a shelf built on it.

    Horizontal position changes show it, and an active pointer pan keeps it visible until release. Only the indicator’s paint hides: the viewport still scrolls, drags and dispatches child controls while it is hidden. The indicator overlays the same content bounds throughout. Without drag_to_pan() no indicator is added. Its default is still Always, with the existing geometry and opacity.

    Motion

    ScrollArea::new("files", content)
        .motion(ScrollMotion { duration_ms: 0, ..ScrollMotion::default() })
        .render()

    A step of the wheel or of a scrollbar button eases to its place over ScrollMotion, 140 ms by default. A duration of zero lands it at once. Dragging the thumb is always direct.

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