1use std::{cell::RefCell, rc::Rc};
5
6use gpui::{
7 AnyElement, App, Bounds, Div, Pixels, Point, Stateful, canvas, deferred, prelude::*, px,
8};
9use motion::{Painter, TAB_SLIDE};
10use theme::Theme;
11use web_time::Instant;
12
13use super::{GAP, Strip};
14
15const SLIDE_FPS: f32 = 120.0;
17
18struct Carried<Id> {
20 id: Id,
21 grab: Pixels,
23 pointer: Option<Pixels>,
25}
26
27struct Slot<Id> {
29 id: Id,
30 home: Option<Pixels>,
32 width: Pixels,
33 painted: Pixels,
35 slide: Option<Slide>,
36}
37
38#[derive(Clone, Copy)]
40struct Slide {
41 from: Pixels,
42 since: Instant,
43}
44
45impl Slide {
46 fn at(self, now: Instant) -> Option<Pixels> {
48 let total = TAB_SLIDE.total().mul_f32(motion::speed_scale());
49 let elapsed = now.saturating_duration_since(self.since);
50 if elapsed >= total {
51 return None;
52 }
53 let progress = TAB_SLIDE.progress(elapsed.as_secs_f32() / total.as_secs_f32());
54 Some(self.from * (1.0 - progress))
55 }
56}
57
58struct State<Id> {
59 painter: Painter,
60 carried: Option<Carried<Id>>,
61 slots: Vec<Slot<Id>>,
62}
63
64impl<Id: PartialEq> State<Id> {
65 fn slot(&self, id: &Id) -> Option<&Slot<Id>> {
66 self.slots.iter().find(|slot| slot.id == *id)
67 }
68
69 fn slot_mut(&mut self, id: &Id) -> Option<&mut Slot<Id>> {
70 self.slots.iter_mut().find(|slot| slot.id == *id)
71 }
72
73 fn carried_offset(&self, id: &Id) -> Option<Pixels> {
75 let carried = self.carried.as_ref().filter(|carried| carried.id == *id)?;
76 let home = self.slot(id).and_then(|slot| slot.home);
77 Some(match (carried.pointer, home) {
78 (Some(pointer), Some(home)) => pointer - carried.grab - home,
79 _ => px(0.0),
80 })
81 }
82}
83
84pub struct Reorder<Id>(Rc<RefCell<State<Id>>>);
117
118impl<Id> Clone for Reorder<Id> {
119 fn clone(&self) -> Self {
120 Self(self.0.clone())
121 }
122}
123
124impl<Id: Clone + PartialEq + 'static> Reorder<Id> {
125 pub fn new(painter: Painter) -> Self {
128 Self(Rc::new(RefCell::new(State {
129 painter,
130 carried: None,
131 slots: Vec::new(),
132 })))
133 }
134
135 pub fn grab(&self, id: Id, at: Point<Pixels>) {
138 self.0.borrow_mut().carried = Some(Carried {
139 id,
140 grab: at.x,
141 pointer: None,
142 });
143 }
144
145 pub fn follow(&self, strip: &mut Strip<Id>, pointer: Point<Pixels>, cx: &App) -> bool {
148 let mut state = self.0.borrow_mut();
149 state.slots.retain(|slot| strip.contains(&slot.id));
150 let Some(carried) = state.carried.as_mut() else {
151 return false;
152 };
153 carried.pointer = Some(pointer.x);
154 let (id, grab) = (carried.id.clone(), carried.grab);
155 let Some(slot) = state.slot(&id) else {
156 return false;
157 };
158 let (Some(home), width) = (slot.home, slot.width) else {
159 return false;
160 };
161
162 let travel = (pointer.x - grab - home).as_f32();
163 let (passed, left) = strip.carry(&id, travel, |tab| {
164 state
165 .slot(tab)
166 .filter(|slot| slot.home.is_some())
167 .map(|slot| slot.width.as_f32())
168 });
169 if passed.is_empty() {
170 return false;
171 }
172
173 let shift = px(travel.signum() * (width.as_f32() + GAP));
176 let now = cx.background_executor().now();
177 let still = cx.reduce_motion();
178 for tab in &passed {
179 if let Some(slot) = state.slot_mut(tab) {
180 slot.home = slot.home.map(|home| home - shift);
181 let seen = slot
182 .slide
183 .and_then(|slide| slide.at(now))
184 .unwrap_or_default();
185 slot.slide = (!still).then_some(Slide {
186 from: seen + shift,
187 since: now,
188 });
189 }
190 }
191 if let Some(slot) = state.slot_mut(&id) {
192 slot.home = Some(pointer.x - grab - px(left));
193 }
194 true
195 }
196
197 pub fn tab(&self, id: &Id, tab: Stateful<Div>, theme: &Theme, cx: &mut App) -> AnyElement {
200 let now = cx.background_executor().now();
201 let mut state = self.0.borrow_mut();
202
203 if !cx.has_active_drag()
205 && let Some(carried) = state.carried.take()
206 {
207 let home = state.slot(&carried.id).and_then(|slot| slot.home);
208 let from = match (carried.pointer, home) {
209 (Some(pointer), Some(home)) => pointer - carried.grab - home,
210 _ => px(0.0),
211 };
212 if let Some(slot) = state.slot_mut(&carried.id)
213 && !cx.reduce_motion()
214 && from != px(0.0)
215 {
216 slot.slide = Some(Slide { from, since: now });
217 }
218 }
219
220 let carried = state.carried_offset(id);
221 if state.slot(id).is_none() {
222 state.slots.push(Slot {
223 id: id.clone(),
224 home: None,
225 width: px(0.0),
226 painted: px(0.0),
227 slide: None,
228 });
229 }
230 let painter = state.painter;
231 let Some(slot) = state.slot_mut(id) else {
232 unreachable!("pushed above");
233 };
234 let offset = match carried {
235 Some(offset) => offset,
236 None => match slot.slide.and_then(|slide| slide.at(now)) {
237 Some(offset) => offset,
238 None => {
239 slot.slide = None;
240 px(0.0)
241 }
242 },
243 };
244 slot.painted = offset;
245 let sliding = slot.slide.is_some();
246 drop(state);
247 if sliding {
248 painter.lease(SLIDE_FPS, TAB_SLIDE.total(), cx);
249 }
250
251 let measure = canvas(
252 {
253 let (state, id) = (self.0.clone(), id.clone());
254 move |bounds: Bounds<Pixels>, _, _| {
255 if let Some(slot) = state.borrow_mut().slot_mut(&id) {
256 slot.home = Some(bounds.left() - slot.painted);
257 slot.width = bounds.size.width;
258 }
259 }
260 },
261 |_, _, _, _| {},
262 )
263 .absolute()
264 .top(px(-1.0))
266 .bottom(px(-1.0))
267 .left(px(-1.0))
268 .right(px(-1.0));
269
270 let tab = tab.relative().left(offset).child(measure);
271 match carried {
272 Some(_) => deferred(tab.bg(theme.surface_raised)).into_any_element(),
273 None => tab.into_any_element(),
274 }
275 }
276}