Skip to content

Drag and drop

Rows can be dragged out of a grid and dropped onto another grid, onto a group caption, or onto any part of the application that takes them. The grid draws the gesture and reports the drop; moving or copying the data is the application’s. The orders example drags selected orders into a basket grid, or onto an account’s caption to move them to that account.

Row dragging is off by default (NoRowDrag, NoRowDrop): without a drop target the gesture would only get in the way of drag-selection. The source grid sets a drag gesture, the target grid a drop policy. orders’ grid does both (the last two lines):

let grid = LiveGridState::new(model::schema(), Orders { commands }, store, cx)
.with_selection_mode(SelectionMode::RowSet)
.with_change_paint(gpui_kit_grid::ChangePaint::Directional(DirectionalChanges))
.with_editor(OrderColumn::Side, SelectEditor::new(options(&model::SIDES)))
.with_editor(OrderColumn::Tif, SelectEditor::new(options(&model::TIFS)))
.with_editor(
OrderColumn::Quantity,
NumberEditor::new().with_min(OwnedValue::Int(1)),
)
.with_editor(OrderColumn::Limit, NumberEditor::new().with_scale(2))
.with_editor(OrderColumn::Expiry, DateEditor::date())
.with_editor(OrderColumn::Active, CheckboxEditor)
.with_editor(OrderColumn::Note, TextEditor::new().with_max_length(80))
.with_row_drag_gesture(SelectedRowsDrag::new())
.with_row_drop_policy(AccountDrop);

and its basket takes drops on its rows:

let basket = cx.new(|cx| {
LiveGridState::new_standalone(basket_schema(), Basket, StoreOptions::default(), cx)
.with_selection_mode(SelectionMode::RowSet)
.with_row_drop_policy(DropOnRows)
});

SelectedRowsDrag is a two-phase gesture: a press on a row that is already selected drags the selection once the pointer is 10 px away (with_threshold changes the distance); a press on an unselected row selects it and drag-selects as before.

LiveGridDelegate::drag_payload adds application data and the drag ghost’s label (DragPayload::new().with_label("3 orders")); the keys always come along.

Policy Lands
NoRowDrop (default) Nowhere: the grid takes no drop
DropOnRows On rows, group captions and the empty area
DropBetweenRows Before or after a row by the half the pointer is in; in a tree the top third is before, the middle onto the row, the bottom third after; onto group captions. Refuses a sorted flat view, where a place between rows would not hold
Your own RowDropPolicy Wherever place says

orders takes drops only on an account’s caption:

/// Drops on the orders grid land on account captions only: the orders move
/// to that account.
struct AccountDrop;
impl RowDropPolicy<Order> for AccountDrop {
fn place(&self, drop: &RowDropContext<'_, Order>) -> Option<DropTarget<u64>> {
match drop.line() {
DropLine::Caption(group)
if group.level_id().as_str() == OrderColumn::Account.id()
&& drop.rows().is_some() =>
{
Some(DropTarget::Group(group.path().clone()))
}
_ => None,
}
}
}

While a drag is over a grid, the target is highlighted, the rows scroll near an edge, and the cursor shows whether the grid takes the drop. Pinned rows, group footers and totals never take one. Escape cancels the drag.

A grid takes rows from grids with the same key type, and values of types registered with with_drop_payload::<T>(), so a panel or another part of the application can be the source or the target.

The grid dropped on emits LiveGridEvent::Drop { source, target, modifiers }. orders moves the dropped orders to the account of the caption:

LiveGridEvent::Drop { source, target, .. } => {
// Orders dropped on an account's caption move to it.
if let (DropSource::Rows(drag), DropTarget::Group(path)) = (source, target)
&& let Some((_, OwnedValue::Text(account))) = path.levels().first()
{
let account = OwnedValue::Text(account.clone());
let writer = self.store.writer();
let snapshot = writer.snapshot();
for key in drag.keys().iter() {
if let Some(order) = snapshot
.get(key)
.and_then(|order| order.with(OrderColumn::Account, &account))
{
writer.upsert(order);
}
}
self.last_drop = Some(
format!(
"Moved {} to {}",
orders_label(drag.keys().len()),
value_text(&account)
)
.into(),
);
}
}

With Option or Ctrl held the cursor is the copy cursor (RowDropPolicy::is_copy changes the rule), but copy or move is still the application’s choice, from modifiers.

  • Rows do not leave the application as files.
  • A drop onto an element in a cell, or onto an open editor, is not taken: GPUI drops only onto the element under the pointer.
  • Starting a drag of 100k selected rows takes about 4 ms on the UI thread.

Apache-2.0, built on gpui-kit. Repository Sponsor More from GigLabo Privacy Licenses