use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use gpui::{App, Pixels, SharedString, Window};
use gpui_base::motion::{Spring, spring};
use super::scoped_records_id;
#[derive(Clone, Copy)]
pub(super) struct RowReorderMotion {
target: Pixels,
pub(super) incarnation: usize,
needs_adopt: bool,
}
const ROW_REORDER_SPRING_EPSILON: f32 = 0.5;
#[cfg(test)]
thread_local! {
static ROW_REORDER_SAMPLE_WRITES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[inline]
fn note_row_reorder_sample_writes(written: usize) {
#[cfg(test)]
ROW_REORDER_SAMPLE_WRITES.with(|writes| writes.set(writes.get().saturating_add(written)));
#[cfg(not(test))]
let _ = written;
}
#[cfg(test)]
pub(super) fn take_row_reorder_sample_writes() -> usize {
ROW_REORDER_SAMPLE_WRITES.with(|writes| writes.replace(0))
}
#[derive(Clone, Default)]
pub(super) struct RowReorderState {
response: Option<Duration>,
motion: HashMap<SharedString, RowReorderMotion>,
offsets: HashMap<SharedString, Pixels>,
visible: HashSet<SharedString>,
generation: usize,
}
impl RowReorderState {
pub(super) fn is_enabled(&self) -> bool {
self.response.is_some()
}
pub(super) fn set_response(&mut self, response: Option<Duration>) {
self.response = response;
if response.is_none() {
self.motion.clear();
self.offsets.clear();
}
}
pub(super) fn animating_len(&self) -> usize {
self.motion.len()
}
pub(super) fn visible_len(&self) -> usize {
self.visible.len()
}
pub(super) fn visible_ids(&self) -> &HashSet<SharedString> {
&self.visible
}
pub(super) fn note_visible(&mut self, row_id: SharedString) {
self.visible.insert(row_id);
}
pub(super) fn sample(
&mut self,
component_id: &str,
row_id: &SharedString,
window: &mut Window,
cx: &mut App,
) -> Option<Pixels> {
let response = self.response?;
let motion = *self.motion.get(row_id)?;
let channel: SharedString = format!(
"{}:{}",
scoped_records_id("reorder", component_id, row_id),
motion.incarnation
)
.into();
let policy = Spring::new(response).with_epsilon(ROW_REORDER_SPRING_EPSILON);
if motion.needs_adopt {
let _ = spring(
(channel.clone(), "position"),
gpui::px(0.),
policy,
window,
cx,
);
if let Some(entry) = self.motion.get_mut(row_id) {
entry.needs_adopt = false;
note_row_reorder_sample_writes(1);
}
}
let sampled = spring((channel, "position"), motion.target, policy, window, cx);
if sampled == motion.target {
self.motion.remove(row_id);
self.offsets.remove(row_id);
note_row_reorder_sample_writes(2);
return None;
}
let offset = sampled - motion.target;
self.offsets.insert(row_id.clone(), offset);
note_row_reorder_sample_writes(1);
Some(offset)
}
pub(super) fn set_visible(&mut self, visible: HashSet<SharedString>) {
self.motion.retain(|row_id, _| visible.contains(row_id));
self.offsets.retain(|row_id, _| visible.contains(row_id));
self.visible = visible;
}
pub(super) fn project<'rows>(
&self,
travelled: impl Iterator<Item = (&'rows SharedString, Pixels)>,
) -> HashMap<SharedString, RowReorderMotion> {
let fresh_incarnation = self.generation.wrapping_add(1);
travelled
.filter_map(|(row_id, displacement)| {
let prior_offset = self
.offsets
.get(row_id)
.copied()
.unwrap_or_else(|| gpui::px(0.));
let motion = match self.motion.get(row_id) {
Some(prior) => RowReorderMotion {
target: prior.target - displacement,
incarnation: prior.incarnation,
needs_adopt: prior.needs_adopt,
},
None => RowReorderMotion {
target: gpui::px(0.) - displacement,
incarnation: fresh_incarnation,
needs_adopt: true,
},
};
let projected_offset = if motion.needs_adopt {
gpui::px(0.) - motion.target
} else {
displacement + prior_offset
};
if projected_offset == gpui::px(0.) {
return None;
}
Some((row_id.clone(), motion))
})
.collect()
}
pub(super) fn accept(&mut self, motion: HashMap<SharedString, RowReorderMotion>) {
self.motion = motion;
self.offsets.clear();
self.generation = self.generation.wrapping_add(1);
}
#[cfg(test)]
pub(super) fn contains_motion(&self, row_id: &str) -> bool {
self.motion.contains_key(row_id)
}
#[cfg(test)]
pub(super) fn sampled_offset(&self, row_id: &str) -> Option<Pixels> {
self.offsets.get(row_id).copied()
}
#[cfg(test)]
pub(super) fn incarnation(&self, row_id: &str) -> Option<usize> {
self.motion.get(row_id).map(|motion| motion.incarnation)
}
#[cfg(test)]
pub(super) fn is_empty(&self) -> bool {
self.motion.is_empty() && self.offsets.is_empty()
}
#[cfg(test)]
pub(super) fn retains_only_visible(&self) -> bool {
self.motion
.keys()
.chain(self.offsets.keys())
.all(|row_id| self.visible.contains(row_id))
}
}