Skip to main content

ReorderableList

Purpose​

A list whose rows can be dragged into a new order.

Signature​

pub struct ReorderableList<Msg = crate::callback::Callback> { .. }

impl ReorderableList {
pub fn new<R: crate::callback::IntoMsg<Msg>>(
on_reorder: impl Fn(usize, usize) -> R + 'static,
) -> Self;
}

Example​

Rendered preview of the ReorderableList demo

From frus-examples/widgets/src/features/scrolling/widgets/reorderable.rs:

//! `ReorderableList` — a list whose rows can be dragged into a new order: a playlist, a
//! set of favourites, a to-do list. The order is the app's; the list reports each move.

use frus::{
Align, Column, Container, Icon, Icons, ListTile, ReorderGrab, ReorderableList, Text, Widget,
};

use crate::common::shared::{section, sections};

pub fn example() -> Box<dyn Widget> {
Box::new(frus::component(|cx| {
let songs = cx.use_state(|| {
vec![
"Blue in Green",
"So What",
"Naima",
"Take Five",
"Round Midnight",
]
});
let tasks = cx.use_state(|| vec!["Write the brief", "Review", "Ship it"]);

// `on_reorder(from, to)`: `to` is where the row ends up, already accounting for
// the row having left its old place — remove, then insert.
let reorder = songs.clone();
let playlist = songs.get().into_iter().enumerate().fold(
ReorderableList::new(move |from: usize, to: usize| {
reorder.update(|list| move_item(list, from, to))
})
// A grip at the trailing edge lifts the row on press; the rest of the row
// keeps its taps and the list keeps scrolling.
.grab(ReorderGrab::Handle)
.gap(4.0),
// A key per row lets the list keep track of a row as it moves.
|list, (i, title)| {
list.keyed_row(
key_of(title),
ListTile::new()
.leading(Icon::new(Icons::MUSIC_NOTE))
.title(title)
.subtitle(format!("Track {}", i + 1)),
)
},
);

// `LongPress`: hold anywhere on the row to lift it — the touch-screen default.
let reorder_tasks = tasks.clone();
let todo = tasks.get().into_iter().fold(
ReorderableList::new(move |from: usize, to: usize| {
reorder_tasks.update(|list| move_item(list, from, to))
})
.grab(ReorderGrab::LongPress),
|list, task| list.keyed_row(key_of(task), ListTile::new().title(task)),
);

sections(vec![
Box::new(section(
"Drag by the handle",
Container::new().width(320.0).child(playlist),
)),
Box::new(section(
"Hold, then drag",
Column::new()
.spacing(8.0)
.cross_axis_alignment(Align::Start)
.child(Container::new().width(320.0).child(todo))
.child(Text::new(format!("Order: {}", tasks.get().join(" → "))).wrap()),
)),
])
}))
}

/// Moves the item at `from` so that it ends up at `to`.
pub fn move_item<T>(list: &mut Vec<T>, from: usize, to: usize) {
if from < list.len() {
let item = list.remove(from);
list.insert(to.min(list.len()), item);
}
}

/// A stable key for a row: here the text itself, which is unique in these lists. A real
/// app uses its own id.
pub fn key_of(text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut hasher);
hasher.finish()
}

Properties​

MethodArgumentEffect
.keyed_row()key: u64, row: impl Widget<Msg> + 'staticAdds a row, identified by key.
.row()row: impl Widget<Msg> + 'staticAdds a row with no key of its own — for a list that never changes length.
.grab()grab: ReorderGrabHow a row is picked up.
.axis()axis: ReorderAxisThe way the rows run, ReorderAxis::Vertical by default. A horizontal list lays its rows out along x, puts each grip under its row, and is reordered across: see the module's notes for what that means under a right-to-left layout.
.enabled()enabled: boolTurns reordering off without changing the list: no grip, no hold, no drop target. A list that is briefly read-only stays the same widget, so its rows keep their state.
.handle()handle: impl Fn(&Theme) -> W + 'staticThe whole grip, replaced by a widget of the application's. It is built with the ambient theme, and what it is made of is nobody's business but the caller's — this widget only says where it goes and what grabbing it means.
.handle_icon()icon: IconDataThe grip's glyph, over the theme's and the framework's.
.handle_color()color: ColorThe grip's colour, over the theme's.
.handle_icon_size()size: f32The grip's glyph size.
.handle_width()width: f32The depth of the grip's box — its tap target, which is not the size of the glyph inside it. A width beside the rows of a vertical list, a height under the rows of a horizontal one.
.gap()gap: f32Space between rows.
.width()width: f32An explicit width.
.width_fraction()fraction: f32A width given as a fraction of what the parent offers.
.height()height: f32An explicit height.
.flex()grow: f32The share of a parent flex's spare room this list takes.

Behaviour​

A list whose rows can be dragged into a new order.

The order stays the application's, as everywhere else here: the list emits this row moved from from to to and the application permutes its own data. Nothing moves on screen until it does.

ReorderableList::new(|from, to| Msg::MoveTrack(from, to))
.gap(8.0)
.keyed_row(track.id, track_row(track))

Each row should carry a key — keyed_row, or a crate::keyed wrapper of your own. Rows are told apart by position otherwise, and a list whose rows are about to swap positions is exactly where that goes wrong: the hover and the animations of the row that moved would stay behind with its index.

See also​

Source: crates/frus-widgets/src/reorderable.rs @ e6fba33f5b42