use std::{cell::RefCell, rc::Rc};
use gpui::{
AnyElement, App, Bounds, Div, Pixels, Point, Stateful, canvas, deferred, prelude::*, px,
};
use motion::{Painter, TAB_SLIDE};
use theme::Theme;
use web_time::Instant;
use super::{GAP, Strip};
const SLIDE_FPS: f32 = 120.0;
struct Carried<Id> {
id: Id,
grab: Pixels,
pointer: Option<Pixels>,
}
struct Slot<Id> {
id: Id,
home: Option<Pixels>,
width: Pixels,
painted: Pixels,
slide: Option<Slide>,
}
#[derive(Clone, Copy)]
struct Slide {
from: Pixels,
since: Instant,
}
impl Slide {
fn at(self, now: Instant) -> Option<Pixels> {
let total = TAB_SLIDE.total().mul_f32(motion::speed_scale());
let elapsed = now.saturating_duration_since(self.since);
if elapsed >= total {
return None;
}
let progress = TAB_SLIDE.progress(elapsed.as_secs_f32() / total.as_secs_f32());
Some(self.from * (1.0 - progress))
}
}
struct State<Id> {
painter: Painter,
carried: Option<Carried<Id>>,
slots: Vec<Slot<Id>>,
}
impl<Id: PartialEq> State<Id> {
fn slot(&self, id: &Id) -> Option<&Slot<Id>> {
self.slots.iter().find(|slot| slot.id == *id)
}
fn slot_mut(&mut self, id: &Id) -> Option<&mut Slot<Id>> {
self.slots.iter_mut().find(|slot| slot.id == *id)
}
fn carried_offset(&self, id: &Id) -> Option<Pixels> {
let carried = self.carried.as_ref().filter(|carried| carried.id == *id)?;
let home = self.slot(id).and_then(|slot| slot.home);
Some(match (carried.pointer, home) {
(Some(pointer), Some(home)) => pointer - carried.grab - home,
_ => px(0.0),
})
}
}
pub struct Reorder<Id>(Rc<RefCell<State<Id>>>);
impl<Id> Clone for Reorder<Id> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<Id: Clone + PartialEq + 'static> Reorder<Id> {
pub fn new(painter: Painter) -> Self {
Self(Rc::new(RefCell::new(State {
painter,
carried: None,
slots: Vec::new(),
})))
}
pub fn grab(&self, id: Id, at: Point<Pixels>) {
self.0.borrow_mut().carried = Some(Carried {
id,
grab: at.x,
pointer: None,
});
}
pub fn follow(&self, strip: &mut Strip<Id>, pointer: Point<Pixels>, cx: &App) -> bool {
let mut state = self.0.borrow_mut();
state.slots.retain(|slot| strip.contains(&slot.id));
let Some(carried) = state.carried.as_mut() else {
return false;
};
carried.pointer = Some(pointer.x);
let (id, grab) = (carried.id.clone(), carried.grab);
let Some(slot) = state.slot(&id) else {
return false;
};
let (Some(home), width) = (slot.home, slot.width) else {
return false;
};
let travel = (pointer.x - grab - home).as_f32();
let (passed, left) = strip.carry(&id, travel, |tab| {
state
.slot(tab)
.filter(|slot| slot.home.is_some())
.map(|slot| slot.width.as_f32())
});
if passed.is_empty() {
return false;
}
let shift = px(travel.signum() * (width.as_f32() + GAP));
let now = cx.background_executor().now();
let still = cx.reduce_motion();
for tab in &passed {
if let Some(slot) = state.slot_mut(tab) {
slot.home = slot.home.map(|home| home - shift);
let seen = slot
.slide
.and_then(|slide| slide.at(now))
.unwrap_or_default();
slot.slide = (!still).then_some(Slide {
from: seen + shift,
since: now,
});
}
}
if let Some(slot) = state.slot_mut(&id) {
slot.home = Some(pointer.x - grab - px(left));
}
true
}
pub fn tab(&self, id: &Id, tab: Stateful<Div>, theme: &Theme, cx: &mut App) -> AnyElement {
let now = cx.background_executor().now();
let mut state = self.0.borrow_mut();
if !cx.has_active_drag()
&& let Some(carried) = state.carried.take()
{
let home = state.slot(&carried.id).and_then(|slot| slot.home);
let from = match (carried.pointer, home) {
(Some(pointer), Some(home)) => pointer - carried.grab - home,
_ => px(0.0),
};
if let Some(slot) = state.slot_mut(&carried.id)
&& !cx.reduce_motion()
&& from != px(0.0)
{
slot.slide = Some(Slide { from, since: now });
}
}
let carried = state.carried_offset(id);
if state.slot(id).is_none() {
state.slots.push(Slot {
id: id.clone(),
home: None,
width: px(0.0),
painted: px(0.0),
slide: None,
});
}
let painter = state.painter;
let Some(slot) = state.slot_mut(id) else {
unreachable!("pushed above");
};
let offset = match carried {
Some(offset) => offset,
None => match slot.slide.and_then(|slide| slide.at(now)) {
Some(offset) => offset,
None => {
slot.slide = None;
px(0.0)
}
},
};
slot.painted = offset;
let sliding = slot.slide.is_some();
drop(state);
if sliding {
painter.lease(SLIDE_FPS, TAB_SLIDE.total(), cx);
}
let measure = canvas(
{
let (state, id) = (self.0.clone(), id.clone());
move |bounds: Bounds<Pixels>, _, _| {
if let Some(slot) = state.borrow_mut().slot_mut(&id) {
slot.home = Some(bounds.left() - slot.painted);
slot.width = bounds.size.width;
}
}
},
|_, _, _, _| {},
)
.absolute()
.top(px(-1.0))
.bottom(px(-1.0))
.left(px(-1.0))
.right(px(-1.0));
let tab = tab.relative().left(offset).child(measure);
match carried {
Some(_) => deferred(tab.bg(theme.surface_raised)).into_any_element(),
None => tab.into_any_element(),
}
}
}