227 lines
8.0 KiB
Rust
227 lines
8.0 KiB
Rust
use std::{collections::HashMap, rc::Rc};
|
|
|
|
use gpui::{
|
|
AnyElement, App, AppContext as _, ElementId, EmptyView, EntityId, InteractiveElement,
|
|
IntoElement, ParentElement, Pixels, RenderOnce, SharedString, StatefulInteractiveElement,
|
|
Styled, Window, canvas, div,
|
|
};
|
|
|
|
use super::Flip;
|
|
|
|
/// A row on its way, by key, and whose list it belongs to.
|
|
struct RowDrag {
|
|
owner: EntityId,
|
|
key: SharedString,
|
|
}
|
|
|
|
/// The row held, by key, where it would land, its top, and where the pointer took it.
|
|
#[derive(Clone)]
|
|
struct Hold {
|
|
key: SharedString,
|
|
target: usize,
|
|
top: Pixels,
|
|
grab: Pixels,
|
|
}
|
|
|
|
type OnReorder = Rc<dyn Fn(usize, usize, &mut Window, &mut App)>;
|
|
|
|
/// Rows reordered by dragging: the held row follows the pointer, the others glide aside, and on drop `on_reorder(from, to)` asks the owner to move row `from` to place `to`.
|
|
#[derive(IntoElement)]
|
|
pub struct Reorder {
|
|
id: ElementId,
|
|
rows: Vec<(SharedString, AnyElement)>,
|
|
on_reorder: Option<OnReorder>,
|
|
}
|
|
|
|
impl Reorder {
|
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
rows: Vec::new(),
|
|
on_reorder: None,
|
|
}
|
|
}
|
|
|
|
pub fn row(mut self, key: impl Into<SharedString>, row: impl IntoElement) -> Self {
|
|
self.rows.push((key.into(), row.into_any_element()));
|
|
self
|
|
}
|
|
|
|
pub fn on_reorder(
|
|
mut self,
|
|
handler: impl Fn(usize, usize, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
self.on_reorder = Some(Rc::new(handler));
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Where the held row lands: past every other row whose middle is above its own.
|
|
fn landing(heights: &[Pixels], from: usize, top: Pixels) -> usize {
|
|
let middle = top + heights[from] / 2.0;
|
|
let mut y = Pixels::ZERO;
|
|
let mut target = 0;
|
|
let others = heights
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(ix, _)| *ix != from)
|
|
.map(|(_, height)| height);
|
|
for height in others {
|
|
if y + *height / 2.0 < middle {
|
|
target += 1;
|
|
}
|
|
y += *height;
|
|
}
|
|
target
|
|
}
|
|
|
|
impl RenderOnce for Reorder {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let hold = window.use_keyed_state((self.id.clone(), "hold"), cx, |_, _| None::<Hold>);
|
|
let sizes = window.use_keyed_state((self.id.clone(), "heights"), cx, |_, _| {
|
|
HashMap::<SharedString, Pixels>::new()
|
|
});
|
|
let owner = hold.entity_id();
|
|
if hold.read(cx).is_some() && !cx.has_active_drag() {
|
|
log::debug!("reorder {:?}: the drag ended elsewhere", self.id);
|
|
hold.update(cx, |hold, _| *hold = None);
|
|
}
|
|
let keys: Vec<SharedString> = self.rows.iter().map(|(key, _)| key.clone()).collect();
|
|
let held = hold.read(cx).clone().and_then(|held| {
|
|
let from = keys.iter().position(|key| *key == held.key);
|
|
if from.is_none() {
|
|
log::info!("reorder: the held row {} left the list", held.key);
|
|
}
|
|
from.map(|from| (from, held.target.min(keys.len() - 1), held.top))
|
|
});
|
|
if held.is_none() && hold.read(cx).is_some() {
|
|
hold.update(cx, |hold, _| *hold = None);
|
|
}
|
|
let heights: Vec<Pixels> = keys
|
|
.iter()
|
|
.map(|key| sizes.read(cx).get(key).copied().unwrap_or_default())
|
|
.collect();
|
|
let mut order: Vec<usize> = (0..keys.len()).collect();
|
|
if let Some((from, target, _)) = held {
|
|
let row = order.remove(from);
|
|
order.insert(target, row);
|
|
}
|
|
let mut rows: Vec<Option<AnyElement>> =
|
|
self.rows.into_iter().map(|(_, row)| Some(row)).collect();
|
|
let mut flip = Flip::new((self.id.clone(), "flip"));
|
|
for ix in order {
|
|
let key = keys[ix].clone();
|
|
let (grab, measured) = (hold.clone(), sizes.clone());
|
|
let row = div()
|
|
.id((self.id.clone(), key.clone()))
|
|
.relative()
|
|
.child(rows[ix].take().expect("each row once"))
|
|
.child(
|
|
canvas(
|
|
{
|
|
let key = key.clone();
|
|
move |bounds, window, cx| {
|
|
if measured.read(cx).get(&key) != Some(&bounds.size.height) {
|
|
measured.update(cx, |sizes, cx| {
|
|
sizes.insert(key.clone(), bounds.size.height);
|
|
cx.notify();
|
|
});
|
|
window.request_animation_frame();
|
|
}
|
|
}
|
|
},
|
|
|_, _, _, _| {},
|
|
)
|
|
.absolute()
|
|
.top_0()
|
|
.left_0()
|
|
.size_full(),
|
|
)
|
|
.on_drag(
|
|
RowDrag {
|
|
owner,
|
|
key: key.clone(),
|
|
},
|
|
move |drag, offset, _, cx| {
|
|
log::info!("reorder: row {} lifted", drag.key);
|
|
grab.update(cx, |hold, cx| {
|
|
*hold = Some(Hold {
|
|
key: drag.key.clone(),
|
|
target: ix,
|
|
top: Pixels::ZERO,
|
|
grab: offset.y,
|
|
});
|
|
cx.notify();
|
|
});
|
|
cx.new(|_| EmptyView)
|
|
},
|
|
);
|
|
flip = flip.row(key, row);
|
|
}
|
|
if let Some((from, _, top)) = held {
|
|
flip = flip.lifted(keys[from].clone(), top);
|
|
}
|
|
let total = heights
|
|
.iter()
|
|
.fold(Pixels::ZERO, |total, height| total + *height);
|
|
let (moves, drops, on_reorder) = (hold.clone(), hold, self.on_reorder);
|
|
let dropped = keys.clone();
|
|
div()
|
|
.id(self.id)
|
|
.child(flip)
|
|
.on_drag_move::<RowDrag>(move |event, _, cx| {
|
|
if event.drag(cx).owner != owner {
|
|
return;
|
|
}
|
|
let pointer = event.event.position.y - event.bounds.top();
|
|
moves.update(cx, |hold, cx| {
|
|
let Some(hold) = hold else { return };
|
|
let Some(from) = keys.iter().position(|key| *key == hold.key) else {
|
|
return;
|
|
};
|
|
let reach = total - heights[from];
|
|
hold.top = (pointer - hold.grab).clamp(Pixels::ZERO, reach.max(Pixels::ZERO));
|
|
hold.target = landing(&heights, from, hold.top);
|
|
cx.notify();
|
|
});
|
|
})
|
|
.on_drop(move |drag: &RowDrag, window, cx| {
|
|
if drag.owner != owner {
|
|
return;
|
|
}
|
|
let Some(held) = drops.update(cx, |hold, cx| {
|
|
cx.notify();
|
|
hold.take()
|
|
}) else {
|
|
return;
|
|
};
|
|
let Some(from) = dropped.iter().position(|key| *key == held.key) else {
|
|
return;
|
|
};
|
|
let target = held.target.min(dropped.len() - 1);
|
|
if from != target {
|
|
log::info!("reorder: row {from} to {target}");
|
|
if let Some(on_reorder) = &on_reorder {
|
|
on_reorder(from, target, window, cx);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use gpui::px;
|
|
|
|
use super::landing;
|
|
|
|
#[test]
|
|
fn a_held_row_lands_past_the_middles_above_it() {
|
|
let heights = [px(20.0); 5];
|
|
assert_eq!(landing(&heights, 0, px(40.0)), 2);
|
|
assert_eq!(landing(&heights, 0, px(0.0)), 0);
|
|
assert_eq!(landing(&heights, 4, px(0.0)), 0);
|
|
assert_eq!(landing(&heights, 2, px(70.0)), 4);
|
|
}
|
|
}
|