Skip to main content

guise/panegroup/
group.rs

1//! The `PaneGroup` gpui component: renders the [`PaneTree`] as nested flex
2//! splits with a draggable divider per split and a tab bar per pane, pulling
3//! each item's content and title from host callbacks (the `SplitPanel`
4//! pattern). Mutating the layout is done through the model methods; user
5//! interactions (activate/close/new tab, and later tear-off) surface as
6//! [`PaneGroupEvent`]s the host reacts to.
7
8use std::collections::HashMap;
9use std::rc::Rc;
10
11use gpui::prelude::*;
12use gpui::{
13    div, px, AnyElement, App, Context, DragMoveEvent, Empty, EntityId, EventEmitter, FocusHandle,
14    IntoElement, MouseButton, SharedString, Window, WindowControlArea,
15};
16
17use crate::style::FlexExt;
18use crate::theme::theme;
19use crate::SplitDirection;
20
21use super::drag::{drop_edge, drop_overlay, DropEdge, TabDrag};
22use super::tree::clamp_ratio;
23use super::{compute_layout, neighbor, Direction, ItemId, Node, Pane, PaneId, PaneIds, PaneTree, Rect, SplitId};
24
25/// Per-item content, re-invoked every render so content stays live.
26type RenderItem = Rc<dyn Fn(ItemId, &mut Window, &mut App) -> AnyElement>;
27/// Per-item title for its tab.
28type ItemTitle = Rc<dyn Fn(ItemId, &App) -> SharedString>;
29
30/// Interactions the host reacts to. The component owns layout; the host owns
31/// items (creating/destroying their real content) and window management.
32#[derive(Clone, Debug)]
33pub enum PaneGroupEvent {
34    /// An item's tab was clicked / it became active.
35    Activated(ItemId),
36    /// An item's close button was clicked; the host should drop the item and
37    /// call [`PaneGroup::close_item`].
38    CloseRequested(ItemId),
39    /// The `+` on a pane's tab bar was clicked; the host should create an item
40    /// and call [`PaneGroup::add_item`] on this pane.
41    NewRequested(PaneId),
42    /// The focused pane changed.
43    FocusChanged(PaneId),
44    /// An item was torn off (via [`PaneGroup::tear_off`]); the host should move
45    /// its content into a new window. The item is already detached from here.
46    TearOff(ItemId),
47}
48
49/// The divider being dragged, identifying its split and owning group.
50#[derive(Clone, Copy)]
51struct DividerDrag {
52    group: EntityId,
53    split: SplitId,
54}
55
56/// A recursive tree of tabbed panes. Construct with a first item, wire content
57/// with [`PaneGroup::on_render_item`] / [`PaneGroup::on_item_title`], subscribe
58/// for [`PaneGroupEvent`]s, and drive layout through the model methods.
59pub struct PaneGroup {
60    tree: PaneTree,
61    panes: HashMap<PaneId, Pane>,
62    ids: PaneIds,
63    focused: PaneId,
64    focus: FocusHandle,
65    render_item: Option<RenderItem>,
66    item_title: Option<ItemTitle>,
67    /// The pane a tab is dragged over + the edge the drop would take (`None` =
68    /// center = add as a tab). Drives the drop overlay; cleared on drop.
69    drag_over: Option<(PaneId, Option<DropEdge>)>,
70    /// When set, the focused pane fills the group (the rest is hidden).
71    zoomed: bool,
72    /// When set (`leading`, `trailing`) px, the group doubles as the window
73    /// titlebar: the top-left pane's tab bar reserves `leading` px on the left
74    /// (for window controls like the macOS traffic lights) and the top-right
75    /// pane's tab bar reserves `trailing` px on the right, with a
76    /// window-draggable filler after its tabs. The host overlays its own
77    /// controls in those insets and renders the group flush to the window top.
78    titlebar: Option<(f32, f32)>,
79}
80
81impl EventEmitter<PaneGroupEvent> for PaneGroup {}
82
83impl PaneGroup {
84    /// A new group with a single pane holding `first`.
85    pub fn new(first: ItemId, cx: &mut Context<Self>) -> Self {
86        let mut ids = PaneIds::new();
87        let root = ids.next();
88        let mut panes = HashMap::new();
89        panes.insert(root, Pane::new(first));
90        Self {
91            tree: PaneTree::new(root),
92            panes,
93            ids,
94            focused: root,
95            focus: cx.focus_handle(),
96            render_item: None,
97            item_title: None,
98            drag_over: None,
99            zoomed: false,
100            titlebar: None,
101        }
102    }
103
104    /// Make the group double as the window titlebar: the top-row tab bars
105    /// reserve `leading`/`trailing` px for the host's window controls and the
106    /// top-right filler becomes a window-drag region. Render the group flush to
107    /// the window top and overlay your controls in the insets.
108    pub fn titlebar(mut self, leading: f32, trailing: f32) -> Self {
109        self.titlebar = Some((leading, trailing));
110        self
111    }
112
113    /// Supply each item's content element (re-invoked every render).
114    pub fn on_render_item(
115        mut self,
116        f: impl Fn(ItemId, &mut Window, &mut App) -> AnyElement + 'static,
117    ) -> Self {
118        self.render_item = Some(Rc::new(f));
119        self
120    }
121
122    /// Supply each item's tab title.
123    pub fn on_item_title(mut self, f: impl Fn(ItemId, &App) -> SharedString + 'static) -> Self {
124        self.item_title = Some(Rc::new(f));
125        self
126    }
127
128    // --- queries --------------------------------------------------------
129
130    pub fn focused_pane(&self) -> PaneId {
131        self.focused
132    }
133
134    /// The active item of the focused pane.
135    pub fn active_item(&self) -> ItemId {
136        self.panes[&self.focused].active()
137    }
138
139    /// Every item across every pane, in pane-layout order.
140    pub fn items(&self) -> Vec<ItemId> {
141        self.tree
142            .panes()
143            .into_iter()
144            .filter_map(|p| self.panes.get(&p))
145            .flat_map(|p| p.items().iter().copied())
146            .collect()
147    }
148
149    pub fn pane_of(&self, item: ItemId) -> Option<PaneId> {
150        self.panes
151            .iter()
152            .find(|(_, p)| p.contains(item))
153            .map(|(&id, _)| id)
154    }
155
156    // --- mutation -------------------------------------------------------
157
158    /// Add `item` as a new tab in `pane` and activate it.
159    pub fn add_item(&mut self, pane: PaneId, item: ItemId, cx: &mut Context<Self>) {
160        if let Some(p) = self.panes.get_mut(&pane) {
161            p.add(item, None);
162            self.set_focus(pane, cx);
163        }
164    }
165
166    /// Add `item` to the focused pane.
167    pub fn add_to_focused(&mut self, item: ItemId, cx: &mut Context<Self>) {
168        let pane = self.focused;
169        self.add_item(pane, item, cx);
170    }
171
172    /// Split `pane` in `dir`, putting `item` in the new pane (on the `first`
173    /// side when true). Returns the new pane id.
174    pub fn split(
175        &mut self,
176        pane: PaneId,
177        dir: SplitDirection,
178        first: bool,
179        item: ItemId,
180        cx: &mut Context<Self>,
181    ) -> PaneId {
182        let new_pane = self.ids.next();
183        if self.tree.split(pane, dir, new_pane, first).is_some() {
184            self.panes.insert(new_pane, Pane::new(item));
185            self.set_focus(new_pane, cx);
186        }
187        new_pane
188    }
189
190    /// Activate `item` in `pane` and focus that pane.
191    pub fn activate(&mut self, pane: PaneId, item: ItemId, cx: &mut Context<Self>) {
192        if let Some(p) = self.panes.get_mut(&pane) {
193            if p.activate_item(item) {
194                self.set_focus(pane, cx);
195                cx.emit(PaneGroupEvent::Activated(item));
196            }
197        }
198    }
199
200    /// Remove `item`; if its pane empties, collapse it out of the tree.
201    pub fn close_item(&mut self, item: ItemId, cx: &mut Context<Self>) {
202        let Some(pane) = self.pane_of(item) else {
203            return;
204        };
205        let emptied = self.panes.get_mut(&pane).map(|p| p.remove(item)).unwrap_or(false);
206        if emptied {
207            self.panes.remove(&pane);
208            // Last pane can't be removed from the tree; keep an empty group
209            // valid by leaving it — but that shouldn't happen (host keeps ≥1).
210            if self.tree.remove(pane) && self.focused == pane {
211                let next = self.tree.panes().first().copied().unwrap_or(pane);
212                self.set_focus(next, cx);
213            }
214        }
215        cx.notify();
216    }
217
218    /// Move `item` onto `to_pane`: `edge = Some(..)` splits that pane and puts
219    /// the item in the new split; `None` adds it as a tab. The item is detached
220    /// from its source pane, collapsing that pane if it empties. Used on tab
221    /// drop.
222    pub fn move_item(
223        &mut self,
224        item: ItemId,
225        to_pane: PaneId,
226        edge: Option<DropEdge>,
227        cx: &mut Context<Self>,
228    ) {
229        self.drag_over = None;
230        let Some(from) = self.pane_of(item) else {
231            cx.notify();
232            return;
233        };
234        let from_single = self.panes.get(&from).is_some_and(|p| p.len() == 1);
235        // Dropping a pane's only item back onto itself is a no-op.
236        if from == to_pane && (edge.is_none() || from_single) {
237            cx.notify();
238            return;
239        }
240        self.detach(from, item);
241        match edge {
242            Some(edge) => {
243                let (axis, first) = edge.split();
244                let new_pane = self.ids.next();
245                if self.tree.split(to_pane, axis, new_pane, first).is_some() {
246                    self.panes.insert(new_pane, Pane::new(item));
247                    self.set_focus(new_pane, cx);
248                    return;
249                }
250                // Target vanished; fall back to a tab in some remaining pane.
251                self.reattach_somewhere(item, cx);
252            }
253            None => {
254                if let Some(p) = self.panes.get_mut(&to_pane) {
255                    p.add(item, None);
256                    self.set_focus(to_pane, cx);
257                } else {
258                    self.reattach_somewhere(item, cx);
259                }
260            }
261        }
262        cx.notify();
263    }
264
265    /// Reorder `item` within its pane to `index` (a tab-bar drop).
266    pub fn reorder_in_pane(&mut self, item: ItemId, index: usize, cx: &mut Context<Self>) {
267        if let Some(pane) = self.pane_of(item) {
268            if let Some(p) = self.panes.get_mut(&pane) {
269                if let Some(from) = p.index_of(item) {
270                    p.reorder(from, index);
271                    cx.notify();
272                }
273            }
274        }
275    }
276
277    /// Remove `item` from `pane`, collapsing the pane out of the tree if empty.
278    fn detach(&mut self, pane: PaneId, item: ItemId) {
279        if let Some(p) = self.panes.get_mut(&pane) {
280            if p.remove(item) {
281                self.panes.remove(&pane);
282                self.tree.remove(pane);
283            }
284        }
285    }
286
287    /// Last-resort: put a detached item back into any surviving pane.
288    fn reattach_somewhere(&mut self, item: ItemId, cx: &mut Context<Self>) {
289        if let Some(&pane) = self.tree.panes().first() {
290            if let Some(p) = self.panes.get_mut(&pane) {
291                p.add(item, None);
292                self.set_focus(pane, cx);
293            }
294        }
295    }
296
297    /// Set the divider ratio of a split.
298    pub fn set_ratio(&mut self, split: SplitId, ratio: f32, cx: &mut Context<Self>) {
299        if self.tree.set_ratio(split, clamp_ratio(ratio)) {
300            cx.notify();
301        }
302    }
303
304    fn set_focus(&mut self, pane: PaneId, cx: &mut Context<Self>) {
305        let changed = self.focused != pane;
306        self.focused = pane;
307        if changed {
308            cx.emit(PaneGroupEvent::FocusChanged(pane));
309        }
310        cx.notify();
311    }
312
313    // --- navigation -----------------------------------------------------
314
315    /// Focus the pane in `dir` from the focused one (via layout geometry).
316    pub fn focus_direction(&mut self, dir: Direction, cx: &mut Context<Self>) {
317        let layout = compute_layout(&self.tree, Rect::new(0.0, 0.0, 1000.0, 1000.0), 0.0);
318        if let Some(pane) = neighbor(&layout, self.focused, dir) {
319            self.set_focus(pane, cx);
320        }
321    }
322
323    /// Activate the next / previous tab in the focused pane (wrapping).
324    pub fn activate_next(&mut self, cx: &mut Context<Self>) {
325        self.cycle_focused(true, cx);
326    }
327
328    pub fn activate_prev(&mut self, cx: &mut Context<Self>) {
329        self.cycle_focused(false, cx);
330    }
331
332    fn cycle_focused(&mut self, next: bool, cx: &mut Context<Self>) {
333        if let Some(p) = self.panes.get_mut(&self.focused) {
334            if next {
335                p.activate_next();
336            } else {
337                p.activate_prev();
338            }
339            let item = p.active();
340            cx.emit(PaneGroupEvent::Activated(item));
341            cx.notify();
342        }
343    }
344
345    // --- split management -----------------------------------------------
346
347    /// Reset every divider to an even split.
348    pub fn equalize(&mut self, cx: &mut Context<Self>) {
349        for (split, _) in self.tree.list_dividers() {
350            self.tree.set_ratio(split, 0.5);
351        }
352        cx.notify();
353    }
354
355    /// Nudge the divider adjacent to the focused pane in a direction by `step`.
356    pub fn resize_focused(&mut self, dir: Direction, step: f32, cx: &mut Context<Self>) {
357        let (axis, delta) = match dir {
358            Direction::Left => (SplitDirection::Horizontal, -step),
359            Direction::Right => (SplitDirection::Horizontal, step),
360            Direction::Up => (SplitDirection::Vertical, -step),
361            Direction::Down => (SplitDirection::Vertical, step),
362        };
363        if let Some(split) = self.tree.nearest_split(self.focused, axis) {
364            if let Some(r) = self.tree.ratio(split) {
365                self.tree.set_ratio(split, r + delta);
366                cx.notify();
367            }
368        }
369    }
370
371    /// Toggle zoom: the focused pane fills the group.
372    pub fn toggle_zoom(&mut self, cx: &mut Context<Self>) {
373        self.zoomed = !self.zoomed;
374        cx.notify();
375    }
376
377    pub fn is_zoomed(&self) -> bool {
378        self.zoomed
379    }
380
381    // --- close / tear-off -----------------------------------------------
382
383    /// Ask the host to close the focused pane's active item (it drops the
384    /// content, then calls [`close_item`](Self::close_item)).
385    pub fn close_focused(&mut self, cx: &mut Context<Self>) {
386        if let Some(p) = self.panes.get(&self.focused) {
387            cx.emit(PaneGroupEvent::CloseRequested(p.active()));
388        }
389    }
390
391    /// Detach `item` and emit [`PaneGroupEvent::TearOff`] so the host can move
392    /// its content to a new window. The host wires the gesture (e.g. a tab
393    /// dragged outside the window, or a menu item).
394    pub fn tear_off(&mut self, item: ItemId, cx: &mut Context<Self>) {
395        if let Some(pane) = self.pane_of(item) {
396            // Don't tear off the group's last remaining item.
397            if self.tree.panes().len() == 1
398                && self.panes.get(&pane).is_some_and(|p| p.len() == 1)
399            {
400                return;
401            }
402            self.detach(pane, item);
403            if !self.tree.contains(self.focused) {
404                if let Some(&p) = self.tree.panes().first() {
405                    self.focused = p;
406                }
407            }
408        }
409        cx.emit(PaneGroupEvent::TearOff(item));
410        cx.notify();
411    }
412
413    // --- persistence accessors ------------------------------------------
414
415    /// The split tree (for serializing the layout).
416    pub fn tree(&self) -> &PaneTree {
417        &self.tree
418    }
419
420    /// The items of a pane, in tab order.
421    pub fn pane_items(&self, pane: PaneId) -> Option<&[ItemId]> {
422        self.panes.get(&pane).map(|p| p.items())
423    }
424}
425
426/// The leaf at the layout's top-left corner (descend `first` always).
427fn top_left(node: &Node) -> PaneId {
428    match node {
429        Node::Leaf(p) => *p,
430        Node::Split { first, .. } => top_left(first),
431    }
432}
433
434/// The leaf at the layout's top-right corner (right child of horizontal splits,
435/// top child of vertical splits).
436fn top_right(node: &Node) -> PaneId {
437    match node {
438        Node::Leaf(p) => *p,
439        Node::Split {
440            axis: SplitDirection::Horizontal,
441            second,
442            ..
443        } => top_right(second),
444        Node::Split { first, .. } => top_right(first),
445    }
446}
447
448impl gpui::Focusable for PaneGroup {
449    fn focus_handle(&self, _cx: &App) -> FocusHandle {
450        self.focus.clone()
451    }
452}
453
454impl Render for PaneGroup {
455    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
456        let root = self.tree.root().clone();
457        let render_item = self.render_item.clone();
458        let item_title = self.item_title.clone();
459        // Zoomed: only the focused pane, filling the group.
460        let inner = if self.zoomed {
461            self.pane_el(self.focused, &render_item, &item_title, window, cx)
462        } else {
463            self.node_el(&root, &render_item, &item_title, window, cx)
464        };
465        div().size_full().track_focus(&self.focus).child(inner)
466    }
467}
468
469impl PaneGroup {
470    /// Render a tree node: a leaf pane, or a split (two children + divider).
471    fn node_el(
472        &self,
473        node: &Node,
474        render_item: &Option<RenderItem>,
475        item_title: &Option<ItemTitle>,
476        window: &mut Window,
477        cx: &mut Context<Self>,
478    ) -> AnyElement {
479        match node {
480            Node::Leaf(pane) => self.pane_el(*pane, render_item, item_title, window, cx),
481            Node::Split {
482                id,
483                axis,
484                ratio,
485                first,
486                second,
487            } => {
488                let horizontal = matches!(axis, SplitDirection::Horizontal);
489                let ratio = *ratio;
490                let split = *id;
491                let group = cx.entity().entity_id();
492                let f = self.node_el(first, render_item, item_title, window, cx);
493                let s = self.node_el(second, render_item, item_title, window, cx);
494
495                let line = theme(cx).border().hsla();
496                let grip = theme(cx).primary().alpha(0.35);
497
498                let first_pane = div()
499                    .flex_basis(px(0.0))
500                    .grow(ratio)
501                    .overflow_hidden()
502                    .child(f);
503                let second_pane = div()
504                    .flex_basis(px(0.0))
505                    .grow(1.0 - ratio)
506                    .overflow_hidden()
507                    .child(s);
508
509                let mut divider = div()
510                    .id(("pg-divider", split.0 as usize))
511                    .flex_none()
512                    .flex()
513                    .items_center()
514                    .justify_center()
515                    .hover(move |st| st.bg(grip))
516                    .on_drag(DividerDrag { group, split }, |_, _off, _w, cx| cx.new(|_| Empty));
517                divider = if horizontal {
518                    divider
519                        .w(px(6.0))
520                        .h_full()
521                        .cursor_col_resize()
522                        .child(div().w(px(1.0)).h_full().bg(line))
523                } else {
524                    divider
525                        .h(px(6.0))
526                        .w_full()
527                        .cursor_row_resize()
528                        .child(div().h(px(1.0)).w_full().bg(line))
529                };
530
531                let mut container = div().size_full().flex().on_drag_move(cx.listener(
532                    move |this, ev: &DragMoveEvent<DividerDrag>, _w, cx| {
533                        let d = ev.drag(cx);
534                        if d.group != group || d.split != split {
535                            return;
536                        }
537                        let b = ev.bounds;
538                        let (pos, extent) = if horizontal {
539                            (f32::from(ev.event.position.x - b.left()), f32::from(b.size.width))
540                        } else {
541                            (f32::from(ev.event.position.y - b.top()), f32::from(b.size.height))
542                        };
543                        if extent > 0.0 {
544                            this.set_ratio(split, pos / extent, cx);
545                        }
546                    },
547                ));
548                container = if horizontal {
549                    container.flex_row()
550                } else {
551                    container.flex_col()
552                };
553                container
554                    .child(first_pane)
555                    .child(divider)
556                    .child(second_pane)
557                    .into_any_element()
558            }
559        }
560    }
561
562    /// Render one pane: its tab bar over the active item's content.
563    fn pane_el(
564        &self,
565        pane: PaneId,
566        render_item: &Option<RenderItem>,
567        item_title: &Option<ItemTitle>,
568        window: &mut Window,
569        cx: &mut Context<Self>,
570    ) -> AnyElement {
571        let Some(p) = self.panes.get(&pane) else {
572            return div().into_any_element();
573        };
574        let t = theme(cx);
575        let surface = t.surface().hsla();
576        let text = t.text().hsla();
577        let border = t.border().hsla();
578        let active_bg = t.surface_hover().hsla();
579
580        let active = p.active();
581        let group = cx.entity().entity_id();
582        let tabs = p.items().iter().copied().enumerate().map(|(i, item)| {
583            let title = item_title
584                .as_ref()
585                .map(|f| f(item, cx))
586                .unwrap_or_else(|| SharedString::from("untitled"));
587            let is_active = item == active;
588            div()
589                .id(("pg-tab", (pane.0 as usize) << 20 | i))
590                .flex()
591                .items_center()
592                .gap_1()
593                .px_2()
594                .h(px(28.0))
595                .when(is_active, |d| d.bg(active_bg))
596                .text_color(text)
597                .hover(|s| s.bg(active_bg))
598                .on_click(cx.listener(move |this, _ev, _w, cx| this.activate(pane, item, cx)))
599                // Drag this tab; drop on a tab to reorder / move-into, or on a
600                // pane body to split (handled on the content wrapper below).
601                .on_drag(
602                    TabDrag {
603                        group,
604                        item,
605                        from_pane: pane,
606                        label: title.clone(),
607                    },
608                    |d, _off, _w, cx| cx.new(|_| d.clone()),
609                )
610                .on_drop(cx.listener(move |this, d: &TabDrag, _w, cx| {
611                    if d.from_pane != pane {
612                        this.move_item(d.item, pane, None, cx);
613                    }
614                    this.reorder_in_pane(d.item, i, cx);
615                }))
616                .child(div().text_size(px(12.0)).child(title))
617                .child(
618                    div()
619                        .id(("pg-tabclose", (pane.0 as usize) << 20 | i))
620                        .text_size(px(12.0))
621                        .text_color(text)
622                        .hover(|s| s.text_color(text))
623                        .child("\u{00d7}")
624                        .on_click(cx.listener(move |_this, _ev, _w, cx| {
625                            cx.emit(PaneGroupEvent::CloseRequested(item));
626                        })),
627                )
628        });
629
630        // Titlebar integration: the top-row tab bars reserve space for the
631        // host's window controls, and the top-right filler drags the window.
632        let is_top_left = self.titlebar.is_some() && top_left(self.tree.root()) == pane;
633        let is_top_right = self.titlebar.is_some() && top_right(self.tree.root()) == pane;
634        let (leading, trailing) = self.titlebar.unwrap_or((0.0, 0.0));
635
636        let mut tab_bar = div()
637            .flex()
638            .flex_row()
639            .items_center()
640            .w_full()
641            .h(px(28.0))
642            .bg(surface)
643            .border_b_1()
644            .border_color(border)
645            .when(is_top_left, |d| d.pl(px(leading)))
646            .when(is_top_right, |d| d.pr(px(trailing)))
647            .children(tabs)
648            .child(
649                div()
650                    .id(("pg-newtab", pane.0 as usize))
651                    .px_2()
652                    .h(px(28.0))
653                    .flex()
654                    .items_center()
655                    .text_color(text)
656                    .hover(|s| s.bg(active_bg))
657                    .child("+")
658                    .on_click(cx.listener(move |_this, _ev, _w, cx| {
659                        cx.emit(PaneGroupEvent::NewRequested(pane));
660                    })),
661            );
662        // A window-drag filler fills the rest of the top row (double-click
663        // zooms, per the platform titlebar convention).
664        if is_top_right {
665            tab_bar = tab_bar.child(
666                div()
667                    .id(("pg-titledrag", pane.0 as usize))
668                    .flex_1()
669                    .h_full()
670                    .window_control_area(WindowControlArea::Drag)
671                    .on_mouse_down(MouseButton::Left, |_, window, _| window.start_window_move()),
672            );
673        }
674
675        let content = render_item
676            .as_ref()
677            .map(|f| f(active, window, cx))
678            .unwrap_or_else(|| div().into_any_element());
679
680        // The drop edge over this pane, if a tab is being dragged onto it.
681        let over = match self.drag_over {
682            Some((p, edge)) if p == pane => Some(edge),
683            _ => None,
684        };
685        let body = div()
686            .relative()
687            .flex_1()
688            .overflow_hidden()
689            .on_drag_move::<TabDrag>(cx.listener(
690                move |this, ev: &DragMoveEvent<TabDrag>, _w, cx| {
691                    let edge = drop_edge(ev.bounds, ev.event.position);
692                    if this.drag_over != Some((pane, edge)) {
693                        this.drag_over = Some((pane, edge));
694                        cx.notify();
695                    }
696                },
697            ))
698            .on_drop(cx.listener(move |this, d: &TabDrag, _w, cx| {
699                let edge = match this.drag_over {
700                    Some((p, e)) if p == pane => e,
701                    _ => None,
702                };
703                this.move_item(d.item, pane, edge, cx);
704            }))
705            .child(content)
706            .when_some(over, |el, edge| el.child(drop_overlay(edge)));
707
708        div()
709            .flex()
710            .flex_col()
711            .size_full()
712            .child(tab_bar)
713            .child(body)
714            .into_any_element()
715    }
716}