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