Components / Selection & input

Multi-select

Markdown crates/ui/src/multi_select.rs docs.rs

Multi-select
use ui::multi_select::{self, Check, Choice, MultiSelect, MultiSelectEvent};

multi_select::init(cx);   // once, at startup, alongside input::init

let labels = cx.new(|cx| {
    MultiSelect::new(choices, cx)
        .with_create(normalize)
        .with_manage()
});

cx.subscribe(&labels, |view, picker, event, cx| match event {
    MultiSelectEvent::Toggled { name, on } => { /* apply, then set_choices */ }
    MultiSelectEvent::Dismissed => { /* close the popover */ }
    _ => {}
})
.detach();

The picker is the popover's body: it paints its own card, and the host mounts it under whatever trigger opens it through one of popover's anchored layers, calling reset as it opens. One picker serves a table cell, a column heading and a toolbar button alike.

The host owns the data. Every event is a request, and the picker paints what set_choices last handed it.

API

// ui::multi_select

/// Once at startup, alongside `input::init`.
pub fn init(cx: &mut App);

/// A colour for a name, the same on every machine.
pub fn tint(theme: &Theme, name: &str) -> Hsla;

pub enum Check { Off, On, Mixed }

pub struct Choice {
    pub name: SharedString,
    pub check: Check,
    /// Painted after the name — how many entries carry it.
    pub count: Option<usize>,
}

impl MultiSelect {
    pub fn new(choices: Vec<Choice>, cx: &mut Context<Self>) -> Self;

    /// A "Create" row for a query naming no choice. `normalize` turns the
    /// query into the name, or `None` for one that cannot be a name.
    pub fn with_create(self, normalize: impl Fn(&str) -> Option<SharedString> + 'static) -> Self;

    /// A `···` on each row, opening a pane to rename or delete it.
    pub fn with_manage(self) -> Self;

    /// Colour names with `tint` instead of `multi_select::tint`.
    pub fn with_tint(self, tint: impl Fn(&str, &Theme) -> Hsla + 'static) -> Self;

    pub fn set_choices(&mut self, choices: Vec<Choice>, cx: &mut Context<Self>);

    /// An empty query, the list pane, focus in the query.
    pub fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>);
}

pub enum MultiSelectEvent {
    Toggled { name: SharedString, on: bool },
    Created(SharedString),
    Renamed { from: SharedString, to: SharedString },
    Deleted(SharedString),
    Dismissed,
}

Choices that are on sit as chips in the query line, each with a ×. Backspace in an empty query turns off the last of them.

Rows show a check for on and a dash for mixed. Choosing a mixed row turns it on. Choosing any row clears the query.

The Create row sits first while the normalised query matches no choice exactly, and Enter takes it. A rename is normalised the same way, and may name a choice that already exists. Delete is reported as asked, unconfirmed.

Keys are up/down, ctrl-p/ctrl-n, enter and escape. Escape in the manage pane returns to the list; in the list it reports Dismissed, as does a press outside the card.

The list shows twelve rows and scrolls past that.