Skip to main content

ui/tabs/
reorder.rs

1//! The horizontal, single-region adapter for [`crate::drag`].
2
3use std::rc::Rc;
4
5use gpui::{
6    App, Axis, Div, ElementId, IntoElement, Pixels, Point, RenderOnce, ScrollHandle, Stateful,
7    Window, div, prelude::*, px,
8};
9
10use super::{GAP, Strip};
11use crate::drag::{self, Domain};
12
13/// Persistent gesture and animation state, one per strip.
14///
15/// Its tabs take no `on_drag` of their own. A control inside a tab stops
16/// mouse-down propagation, as [`super::close`] does, or a press on it picks up
17/// the tab.
18pub struct Reorder<Id> {
19    domain: Domain<(), Id>,
20    scroll: ScrollHandle,
21}
22
23impl<Id> Clone for Reorder<Id> {
24    fn clone(&self) -> Self {
25        Self {
26            domain: self.domain.clone(),
27            scroll: self.scroll.clone(),
28        }
29    }
30}
31
32impl<Id: Clone + PartialEq + 'static> Default for Reorder<Id> {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl<Id: Clone + PartialEq + 'static> Reorder<Id> {
39    pub fn new() -> Self {
40        Self {
41            domain: Domain::new(),
42            scroll: ScrollHandle::new(),
43        }
44    }
45
46    /// Supply tabs in model order, each with a key stable across renders. The
47    /// preview moves live; host order changes on release.
48    pub fn bar(
49        &self,
50        id: impl Into<ElementId>,
51        strip: &Strip<Id>,
52        tabs: impl IntoIterator<Item = (Id, Stateful<Div>)>,
53    ) -> Bar<Id> {
54        let tabs: Vec<_> = tabs.into_iter().collect();
55        debug_assert!(tabs.iter().map(|(id, _)| id).eq(strip.tabs()));
56        if self
57            .domain
58            .carried()
59            .is_some_and(|carried| !strip.contains(&carried))
60        {
61            self.domain.invalidate();
62        }
63        Bar {
64            reorder: self.clone(),
65            id: id.into(),
66            tabs,
67            moved: None,
68            outside: None,
69        }
70    }
71}
72
73/// A committed move. The active tab remains the host's choice.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct Move {
76    pub from: usize,
77    pub to: usize,
78}
79
80/// A carried tab released outside its strip; local reordering is cancelled.
81#[derive(Clone, Debug)]
82pub struct OutsideDrop<Id> {
83    pub id: Id,
84    pub position: Point<Pixels>,
85}
86
87type OnMove = dyn Fn(&Move, &mut Window, &mut App);
88type OnOutside<Id> = dyn Fn(&OutsideDrop<Id>, &mut Window, &mut App);
89
90#[derive(IntoElement)]
91pub struct Bar<Id: Clone + PartialEq + 'static> {
92    reorder: Reorder<Id>,
93    id: ElementId,
94    tabs: Vec<(Id, Stateful<Div>)>,
95    moved: Option<Rc<OnMove>>,
96    outside: Option<Rc<OnOutside<Id>>>,
97}
98
99impl<Id: Clone + PartialEq + 'static> Bar<Id> {
100    /// Apply the final move synchronously. Accepts `cx.listener`.
101    pub fn on_reorder(mut self, moved: impl Fn(&Move, &mut Window, &mut App) + 'static) -> Self {
102        self.moved = Some(Rc::new(moved));
103        self
104    }
105
106    pub fn on_drop_outside(
107        mut self,
108        outside: impl Fn(&OutsideDrop<Id>, &mut Window, &mut App) + 'static,
109    ) -> Self {
110        self.outside = Some(Rc::new(outside));
111        self
112    }
113}
114
115impl<Id: Clone + PartialEq + 'static> RenderOnce for Bar<Id> {
116    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
117        let raised = theme::Theme::of(cx).surface_raised;
118        let Reorder { domain, scroll } = self.reorder;
119        let order: Vec<Id> = self.tabs.iter().map(|(id, _)| id.clone()).collect();
120        let row = div()
121            .id("tab-row")
122            .flex_1()
123            .min_w_0()
124            .flex()
125            .flex_row()
126            .items_center()
127            .gap(px(GAP))
128            .overflow_x_scroll()
129            .track_scroll(&scroll)
130            .children(self.tabs.into_iter().map(|(id, tab)| {
131                let tab = tab.flex_none();
132                let tab = match domain.carries(&id) {
133                    true => tab.bg(raised).cursor_grabbing(),
134                    false => tab,
135                };
136                domain.handle(id, tab)
137            }));
138        let mut region = domain
139            .region("tab-region", (), Axis::Horizontal, row)
140            .axis_locked()
141            .track_scroll(&scroll)
142            .flex_1()
143            .min_w_0()
144            .min_h_0()
145            .flex();
146        if let Some(moved) = self.moved {
147            region = region.on_drop(move |event: &drag::Drop<(), Id>, window, cx| {
148                let Some(from) = order.iter().position(|id| *id == event.item) else {
149                    return;
150                };
151                let rest: Vec<&Id> = order.iter().filter(|id| **id != event.item).collect();
152                let to = match (&event.after, &event.before) {
153                    (Some(after), _) => rest.iter().position(|id| *id == after).map(|at| at + 1),
154                    (None, Some(before)) => rest.iter().position(|id| *id == before),
155                    (None, None) => Some(0),
156                };
157                if let Some(to) = to {
158                    moved(&Move { from, to }, window, cx);
159                }
160            });
161        }
162        if let Some(outside) = self.outside {
163            region = region.on_drop_outside(move |event: &drag::Outside<Id>, window, cx| {
164                outside(
165                    &OutsideDrop {
166                        id: event.item.clone(),
167                        position: event.position,
168                    },
169                    window,
170                    cx,
171                );
172            });
173        }
174        // Sized to its tabs: the bar grows into this box, not the row it sits in.
175        div()
176            .id(self.id)
177            .relative()
178            .min_w_0()
179            .min_h_0()
180            .flex()
181            .flex_row()
182            .child(super::bar("tab-bar").viewport(region.into_any_element(), &scroll))
183    }
184}