1use std::{cell::RefCell, rc::Rc};
4use web_time::Instant;
5
6use gpui::{
7 App, Axis, Bounds, Corners, Div, ElementId, IntoElement, Pixels, Point, RenderOnce,
8 ScrollHandle, Stateful, Window, canvas, div, fill, prelude::*, px, relative, size,
9 transparent_black,
10};
11use motion::TAB_SLIDE;
12use theme::Theme;
13
14use super::{GAP, Strip, TRACK_HEIGHT, TRACK_MIN_WIDTH, TRACK_PAD, Track};
15use crate::drag::{self, Domain};
16use crate::widgets;
17
18pub struct Reorder<Id> {
24 domain: Domain<(), Id>,
25 scroll: ScrollHandle,
26 pill: Rc<RefCell<Pill<Id>>>,
27}
28
29impl<Id> Clone for Reorder<Id> {
30 fn clone(&self) -> Self {
31 Self {
32 domain: self.domain.clone(),
33 scroll: self.scroll.clone(),
34 pill: self.pill.clone(),
35 }
36 }
37}
38
39struct Pill<Id> {
42 places: Vec<(Id, Bounds<Pixels>)>,
44 active: Option<Id>,
46 drawn: Option<Bounds<Pixels>>,
47 from: Option<(Bounds<Pixels>, Instant)>,
49}
50
51impl<Id> Default for Pill<Id> {
52 fn default() -> Self {
53 Self {
54 places: Vec::new(),
55 active: None,
56 drawn: None,
57 from: None,
58 }
59 }
60}
61
62impl<Id: Clone + PartialEq> Pill<Id> {
63 fn place(
67 &mut self,
68 active: Option<&Id>,
69 origin: Point<Pixels>,
70 cx: &App,
71 ) -> (Option<Bounds<Pixels>>, bool) {
72 if self.active.as_ref() != active {
73 self.from = self
74 .drawn
75 .map(|drawn| (drawn, cx.background_executor().now()));
76 self.active = active.cloned();
77 }
78 let to = self
79 .places
80 .iter()
81 .find(|(id, _)| Some(id) == active)
82 .map(|(_, tab)| Bounds::new(tab.origin - origin, tab.size));
83 let Some(to) = to else {
84 self.drawn = None;
85 return (None, false);
86 };
87 let t = match self.from {
88 Some((_, since)) if !cx.reduce_motion() => {
89 let elapsed = cx
90 .background_executor()
91 .now()
92 .saturating_duration_since(since);
93 let total = TAB_SLIDE.total().mul_f32(motion::speed_scale());
94 TAB_SLIDE.progress(elapsed.as_secs_f32() / total.as_secs_f32().max(f32::EPSILON))
95 }
96 _ => 1.,
97 };
98 let at = match self.from {
99 Some((from, _)) if t < 1. => Bounds::new(
100 from.origin + (to.origin - from.origin) * t,
101 size(
102 from.size.width + (to.size.width - from.size.width) * t,
103 from.size.height + (to.size.height - from.size.height) * t,
104 ),
105 ),
106 _ => {
107 self.from = None;
108 to
109 }
110 };
111 self.drawn = Some(at);
112 (Some(at), self.from.is_some())
113 }
114}
115
116impl<Id: Clone + PartialEq + 'static> Default for Reorder<Id> {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl<Id: Clone + PartialEq + 'static> Reorder<Id> {
123 pub fn new() -> Self {
124 Self {
125 domain: Domain::new(),
126 scroll: ScrollHandle::new(),
127 pill: Rc::default(),
128 }
129 }
130
131 pub fn bar(
134 &self,
135 id: impl Into<ElementId>,
136 strip: &Strip<Id>,
137 tabs: impl IntoIterator<Item = (Id, Stateful<Div>)>,
138 ) -> Bar<Id> {
139 let tabs: Vec<_> = tabs.into_iter().collect();
140 debug_assert!(tabs.iter().map(|(id, _)| id).eq(strip.tabs()));
141 if self
142 .domain
143 .carried()
144 .is_some_and(|carried| !strip.contains(&carried))
145 {
146 self.domain.invalidate();
147 }
148 Bar {
149 reorder: self.clone(),
150 id: id.into(),
151 tabs,
152 active: strip.active().cloned(),
153 track: None,
154 moved: None,
155 outside: None,
156 }
157 }
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
162pub struct Move {
163 pub from: usize,
164 pub to: usize,
165}
166
167#[derive(Clone, Debug)]
169pub struct OutsideDrop<Id> {
170 pub id: Id,
171 pub position: Point<Pixels>,
172}
173
174type OnMove = dyn Fn(&Move, &mut Window, &mut App);
175type OnOutside<Id> = dyn Fn(&OutsideDrop<Id>, &mut Window, &mut App);
176
177#[derive(IntoElement)]
178pub struct Bar<Id: Clone + PartialEq + 'static> {
179 reorder: Reorder<Id>,
180 id: ElementId,
181 tabs: Vec<(Id, Stateful<Div>)>,
182 active: Option<Id>,
183 track: Option<Track>,
184 moved: Option<Rc<OnMove>>,
185 outside: Option<Rc<OnOutside<Id>>>,
186}
187
188impl<Id: Clone + PartialEq + 'static> Bar<Id> {
189 pub fn on_track(mut self, track: Track) -> Self {
191 self.track = Some(track);
192 self
193 }
194
195 pub fn on_reorder(mut self, moved: impl Fn(&Move, &mut Window, &mut App) + 'static) -> Self {
197 self.moved = Some(Rc::new(moved));
198 self
199 }
200
201 pub fn on_drop_outside(
202 mut self,
203 outside: impl Fn(&OutsideDrop<Id>, &mut Window, &mut App) + 'static,
204 ) -> Self {
205 self.outside = Some(Rc::new(outside));
206 self
207 }
208}
209
210impl<Id: Clone + PartialEq + 'static> RenderOnce for Bar<Id> {
211 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
212 let theme = Theme::of(cx).clone();
213 let raised = theme.surface_raised;
214 let Reorder {
215 domain,
216 scroll,
217 pill,
218 } = self.reorder;
219 let order: Vec<Id> = self.tabs.iter().map(|(id, _)| id.clone()).collect();
220 let (track, active) = (self.track, self.active);
221 pill.borrow_mut()
222 .places
223 .retain(|(id, _)| order.contains(id));
224 let flat = track == Some(Track::Flat);
225 let under = track.map(|_| {
229 let (pill, active, theme) = (pill.clone(), active.clone(), theme.clone());
230 canvas(
231 |_, _, _| {},
232 move |row, _, window, cx| {
233 let (at, sliding) = pill.borrow_mut().place(active.as_ref(), row.origin, cx);
234 if sliding {
235 window.request_animation_frame();
236 }
237 let Some(at) = at else {
238 return;
239 };
240 let bounds = Bounds::new(row.origin + at.origin, at.size);
241 let corners = Corners::all(match flat {
242 true => px(Theme::control_radius()),
243 false => at.size.height / 2.,
244 });
245 window.paint_quad(fill(bounds, theme.card_selected_bg()).corner_radii(corners));
246 window.paint_inset_shadows(bounds, corners, &theme::glass_selected_shadows());
247 },
248 )
249 .absolute()
250 .top_0()
251 .left_0()
252 .size_full()
253 });
254 let parted = |at: usize| {
258 at > 0 && active.as_ref() != order.get(at) && active.as_ref() != order.get(at - 1)
259 };
260 let row = div()
261 .id("tab-row")
262 .flex_1()
263 .min_w_0()
264 .flex()
265 .flex_row()
266 .items_center()
267 .gap(px(GAP))
268 .overflow_x_scroll()
269 .track_scroll(&scroll)
270 .children(under)
271 .children(self.tabs.into_iter().enumerate().map(|(at, (id, tab))| {
272 let shown = track.is_some() && active.as_ref() == Some(&id);
273 let measure = {
274 let (pill, id) = (pill.clone(), id.clone());
275 canvas(
276 move |bounds, _, _| {
277 let mut pill = pill.borrow_mut();
278 match pill.places.iter_mut().find(|(at, _)| *at == id) {
279 Some((_, place)) => *place = bounds,
280 None => pill.places.push((id, bounds)),
281 }
282 },
283 |_, _, _, _| {},
284 )
285 .absolute()
286 .size_full()
287 };
288 let tab = match track {
289 None => tab.flex_none(),
290 Some(_) => tab
291 .when(shown, |tab| tab.bg(transparent_black()).shadow(Vec::new()))
292 .child(measure)
293 .relative()
294 .flex_1()
295 .min_w(px(TRACK_MIN_WIDTH))
296 .h(px(widgets::on_track_height(TRACK_HEIGHT, TRACK_PAD)))
297 .max_w(relative(1.))
298 .justify_center()
299 .map(|tab| match flat {
300 true => tab.rounded(px(Theme::control_radius())),
301 false => tab.rounded_full(),
302 })
303 .when(parted(at), |tab| {
304 tab.child(
305 div()
306 .absolute()
307 .left(px(-(1.0 + GAP / 2.0) - 0.5))
310 .top(px(5.0))
311 .bottom(px(5.0))
312 .w(px(1.0))
313 .bg(theme.border),
314 )
315 }),
316 };
317 let tab = match domain.carries(&id) {
318 true => tab.bg(raised).cursor_grabbing(),
319 false => tab,
320 };
321 domain.handle(id, tab)
322 }));
323 let mut region = domain
324 .region("tab-region", (), Axis::Horizontal, row)
325 .axis_locked()
326 .track_scroll(&scroll)
327 .flex_1()
328 .min_w_0()
329 .min_h_0()
330 .flex();
331 if let Some(moved) = self.moved {
332 region = region.on_drop(move |event: &drag::Drop<(), Id>, window, cx| {
333 let Some(from) = order.iter().position(|id| *id == event.item) else {
334 return;
335 };
336 let rest: Vec<&Id> = order.iter().filter(|id| **id != event.item).collect();
337 let to = match (&event.after, &event.before) {
338 (Some(after), _) => rest.iter().position(|id| *id == after).map(|at| at + 1),
339 (None, Some(before)) => rest.iter().position(|id| *id == before),
340 (None, None) => Some(0),
341 };
342 if let Some(to) = to {
343 moved(&Move { from, to }, window, cx);
344 }
345 });
346 }
347 if let Some(outside) = self.outside {
348 region = region.on_drop_outside(move |event: &drag::Outside<Id>, window, cx| {
349 outside(
350 &OutsideDrop {
351 id: event.item.clone(),
352 position: event.position,
353 },
354 window,
355 cx,
356 );
357 });
358 }
359 let frame = match track {
362 None => div(),
363 Some(track) => widgets::track(&theme)
364 .p(px(TRACK_PAD))
365 .h(px(TRACK_HEIGHT))
366 .rounded_full()
367 .flex_1()
368 .when(track != Track::Bordered, |frame| {
369 frame.border_color(transparent_black())
370 })
371 .when(track == Track::Flat, |frame| frame.bg(transparent_black())),
372 };
373 frame
374 .id(self.id)
375 .relative()
376 .min_w_0()
377 .min_h_0()
378 .flex()
379 .flex_row()
380 .child(super::bar("tab-bar").viewport(region.into_any_element(), &scroll))
381 }
382}