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